@bedrock-rbx/core 0.1.0-beta.11 → 0.1.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/run.mjs +54 -210
- package/dist/cli/run.mjs.map +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/{define-config-87u2jqjM.d.mts → define-config-Bd0XIiSX.d.mts} +155 -8
- package/dist/{define-config-87u2jqjM.d.mts.map → define-config-Bd0XIiSX.d.mts.map} +1 -1
- package/dist/index.d.mts +1847 -1637
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +26 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-mantle-state-DjAU-I39.mjs → migrate-mantle-state-CQjWBZwT.mjs} +857 -79
- package/dist/migrate-mantle-state-CQjWBZwT.mjs.map +1 -0
- package/package.json +4 -4
- package/dist/migrate-mantle-state-DjAU-I39.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { C as ConfigError, S as validateConfig, _ as
|
|
2
|
-
import { Type as Type$1 } from "arktype";
|
|
1
|
+
import { C as UniverseOverlayWithoutId, D as ConfigValidationIssue, E as ConfigError, S as UniverseOverlayWithId, T as validateConfig, _ as ResolvedPlaceEntry, a as ConfigEnvironmentUniverseId, b as StateConfig, c as DisplayNamePrefixConfig, d as GistStateConfig, f as PlaceEntry, g as ResolvedConfig, h as RedactedPlaceOverride, i as Config, l as EnvironmentEntry, m as RedactedGamePassOverride, n as ConfigInput, o as ConfigRootUniverseId, p as RedactedDeveloperProductOverride, r as defineConfig, s as DeveloperProductEntry, t as ConfigContext, u as GamePassEntry, v as ResolvedUniverseEntry, w as isGistStateConfig, x as UniverseEntry, y as ResourceEntryByKind } from "./define-config-Bd0XIiSX.mjs";
|
|
3
2
|
import { OpenCloudError, OpenCloudError as OpenCloudError$1, Result, Result as Result$1 } from "@bedrock-rbx/ocale";
|
|
3
|
+
import { Type as Type$1 } from "arktype";
|
|
4
4
|
import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
|
|
5
5
|
import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
|
|
6
6
|
import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
@@ -708,17 +708,16 @@ type Prettify<T> = { readonly [K in keyof T]: T[K] };
|
|
|
708
708
|
*/
|
|
709
709
|
declare const UNIVERSE_SINGLETON_KEY: ResourceKey;
|
|
710
710
|
//#endregion
|
|
711
|
-
//#region src/
|
|
711
|
+
//#region src/core/state.d.ts
|
|
712
712
|
/**
|
|
713
|
-
*
|
|
714
|
-
* implements to teach Bedrock how to reconcile one {@link ResourceKind} against
|
|
715
|
-
* its upstream API.
|
|
713
|
+
* In-memory state snapshot for one environment.
|
|
716
714
|
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
715
|
+
* The on-disk JSON wraps this shape with a `$bedrock: { version: N }` envelope.
|
|
716
|
+
* Adapters flatten the envelope on read and re-wrap it on write; nothing
|
|
717
|
+
* outside an adapter sees the `$bedrock` key.
|
|
720
718
|
*
|
|
721
|
-
*
|
|
719
|
+
* `version` is a literal so a breaking schema change is a compile-time type
|
|
720
|
+
* shift rather than a silently accepted runtime value.
|
|
722
721
|
*
|
|
723
722
|
* @example
|
|
724
723
|
*
|
|
@@ -727,820 +726,729 @@ declare const UNIVERSE_SINGLETON_KEY: ResourceKey;
|
|
|
727
726
|
* asResourceKey,
|
|
728
727
|
* asRobloxAssetId,
|
|
729
728
|
* asSha256Hex,
|
|
730
|
-
* type
|
|
729
|
+
* type BedrockState,
|
|
731
730
|
* } from "@bedrock-rbx/core";
|
|
732
731
|
*
|
|
733
|
-
* const
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
732
|
+
* const state: BedrockState = {
|
|
733
|
+
* environment: "production",
|
|
734
|
+
* resources: [
|
|
735
|
+
* {
|
|
736
|
+
* description: "Grants VIP perks.",
|
|
737
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
738
|
+
* iconFileHashes: {
|
|
739
|
+
* "en-us": asSha256Hex(
|
|
740
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
741
|
+
* ),
|
|
742
742
|
* },
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
743
|
+
* key: asResourceKey("vip-pass"),
|
|
744
|
+
* kind: "gamePass",
|
|
745
|
+
* name: "VIP Pass",
|
|
746
|
+
* outputs: {
|
|
747
|
+
* assetId: asRobloxAssetId("9876543210"),
|
|
748
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
|
|
749
|
+
* },
|
|
750
|
+
* price: 500,
|
|
751
|
+
* },
|
|
752
|
+
* ],
|
|
753
|
+
* version: 1,
|
|
746
754
|
* };
|
|
747
755
|
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
* description: "Grants VIP perks.",
|
|
751
|
-
* icon: { "en-us": "assets/vip-icon.png" },
|
|
752
|
-
* iconFileHashes: {
|
|
753
|
-
* "en-us": asSha256Hex(
|
|
754
|
-
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
755
|
-
* ),
|
|
756
|
-
* },
|
|
757
|
-
* key: asResourceKey("vip-pass"),
|
|
758
|
-
* kind: "gamePass",
|
|
759
|
-
* name: "VIP Pass",
|
|
760
|
-
* price: undefined,
|
|
761
|
-
* })
|
|
762
|
-
* .then((result) => {
|
|
763
|
-
* expect(result.success).toBeTrue();
|
|
764
|
-
* if (result.success) {
|
|
765
|
-
* expect(result.data.outputs.assetId).toBe("9876543210");
|
|
766
|
-
* }
|
|
767
|
-
* });
|
|
756
|
+
* expect(state.version).toBe(1);
|
|
757
|
+
* expect(state.resources).toHaveLength(1);
|
|
768
758
|
* ```
|
|
769
759
|
*/
|
|
770
|
-
interface
|
|
771
|
-
/**
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
|
|
778
|
-
/**
|
|
779
|
-
* Reconcile an upstream resource whose managed content has drifted from its
|
|
780
|
-
* desired state. Receives the last-known current state so the driver can
|
|
781
|
-
* compute a minimal patch (or no-op upstream, for file-backed kinds where
|
|
782
|
-
* republishing is unconditional).
|
|
783
|
-
*
|
|
784
|
-
* Optional. Drivers whose upstream API has no update operation omit this
|
|
785
|
-
* method; `applyOps` surfaces an `updateUnsupported` error at dispatch time
|
|
786
|
-
* instead.
|
|
787
|
-
*/
|
|
788
|
-
update?(current: ResourceCurrentState<K>, desired: Extract<ResourceDesiredState, {
|
|
789
|
-
kind: K;
|
|
790
|
-
}>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
|
|
760
|
+
interface BedrockState {
|
|
761
|
+
/** Environment name this snapshot belongs to (e.g. `"production"`, `"staging"`). */
|
|
762
|
+
readonly environment: string;
|
|
763
|
+
/** Current state of every resource Bedrock manages in this environment. */
|
|
764
|
+
readonly resources: ReadonlyArray<ResourceCurrentState>;
|
|
765
|
+
/** Schema-version literal; bumped only for breaking changes to the on-disk format. */
|
|
766
|
+
readonly version: 1;
|
|
791
767
|
}
|
|
792
768
|
/**
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
* a
|
|
769
|
+
* Failure surfaced by a `StatePort` when a state file exists but cannot be
|
|
770
|
+
* trusted: corrupt JSON, schema failure, or an unknown `$bedrock.version`.
|
|
771
|
+
*
|
|
772
|
+
* Narrow on `kind` rather than using `instanceof`: `StateError` is plain data,
|
|
773
|
+
* not a thrown error subclass.
|
|
798
774
|
*
|
|
799
775
|
* @example
|
|
800
776
|
*
|
|
801
777
|
* ```ts
|
|
802
|
-
* import {
|
|
778
|
+
* import type { StateError } from "@bedrock-rbx/core";
|
|
803
779
|
*
|
|
804
|
-
* const
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
* },
|
|
809
|
-
* },
|
|
810
|
-
* place: {
|
|
811
|
-
* async create() {
|
|
812
|
-
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
813
|
-
* },
|
|
814
|
-
* },
|
|
815
|
-
* universe: {
|
|
816
|
-
* async create() {
|
|
817
|
-
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
818
|
-
* },
|
|
819
|
-
* },
|
|
820
|
-
* developerProduct: {
|
|
821
|
-
* async create() {
|
|
822
|
-
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
823
|
-
* },
|
|
824
|
-
* },
|
|
780
|
+
* const err: StateError = {
|
|
781
|
+
* file: ".bedrock/state/production.json",
|
|
782
|
+
* kind: "stateError",
|
|
783
|
+
* reason: "Corrupt JSON: unexpected token at line 1 column 5",
|
|
825
784
|
* };
|
|
826
785
|
*
|
|
827
|
-
* expect(
|
|
786
|
+
* expect(err.kind).toBe("stateError");
|
|
828
787
|
* ```
|
|
829
788
|
*/
|
|
830
|
-
|
|
789
|
+
interface StateError {
|
|
790
|
+
/** Adapter-specific path or identifier of the file that failed to parse. */
|
|
791
|
+
readonly file: string;
|
|
792
|
+
/** Literal discriminator for narrowing. */
|
|
793
|
+
readonly kind: "stateError";
|
|
794
|
+
/** Human-readable explanation of why the file could not be trusted. */
|
|
795
|
+
readonly reason: string;
|
|
796
|
+
}
|
|
831
797
|
//#endregion
|
|
832
|
-
//#region src/
|
|
798
|
+
//#region src/core/migrate/migration-report.d.ts
|
|
833
799
|
/**
|
|
834
|
-
*
|
|
835
|
-
* at construction time (matching `GamePassDriverDeps`) so each driver
|
|
836
|
-
* instance is bound to a single universe; multi-universe deploys construct
|
|
837
|
-
* one driver per universe. `readFile` exists on the driver (not upstream
|
|
838
|
-
* in shell) because icon hashes flow through `diff` but bytes do not.
|
|
839
|
-
*
|
|
840
|
-
* @example
|
|
841
|
-
*
|
|
842
|
-
* ```ts
|
|
843
|
-
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
844
|
-
* import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
|
|
845
|
-
* import { asRobloxAssetId, type DeveloperProductDriverDeps } from "@bedrock-rbx/core";
|
|
846
|
-
*
|
|
847
|
-
* const httpClient: HttpClient = {
|
|
848
|
-
* async request() {
|
|
849
|
-
* return { data: { body: {}, headers: {}, status: 200 }, success: true };
|
|
850
|
-
* },
|
|
851
|
-
* };
|
|
852
|
-
*
|
|
853
|
-
* const deps: DeveloperProductDriverDeps = {
|
|
854
|
-
* client: new DeveloperProductsClient({
|
|
855
|
-
* apiKey: "rbx-your-key",
|
|
856
|
-
* httpClient,
|
|
857
|
-
* sleep: async () => {},
|
|
858
|
-
* }),
|
|
859
|
-
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
860
|
-
* universeId: asRobloxAssetId("1234567890"),
|
|
861
|
-
* };
|
|
800
|
+
* Per-environment in-memory state snapshot map keyed by environment name.
|
|
862
801
|
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
802
|
+
* `Record` rather than the PRD-suggested `Map` so the field survives
|
|
803
|
+
* `JSON.stringify` for downstream logging and parallel-iterates cleanly
|
|
804
|
+
* with `Config.environments` (which is itself a `Record`).
|
|
865
805
|
*/
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
806
|
+
type StatesByEnvironment = Readonly<Record<string, BedrockState>>;
|
|
807
|
+
/**
|
|
808
|
+
* Aggregate counts for the four `MigrationWarning` kinds. Computed by
|
|
809
|
+
* folding `MigrationReport.warnings`; lets a CI gate skim totals without
|
|
810
|
+
* iterating every entry. All fields are zero on a clean migration.
|
|
811
|
+
*/
|
|
812
|
+
interface MigrationSummary {
|
|
813
|
+
/** Number of `ambiguous` warnings emitted. */
|
|
814
|
+
readonly ambiguousCount: number;
|
|
815
|
+
/** Number of `blocked` warnings emitted. */
|
|
816
|
+
readonly blockedCount: number;
|
|
817
|
+
/** Number of `deferred` warnings emitted. */
|
|
818
|
+
readonly deferredCount: number;
|
|
819
|
+
/** Number of `interpretive` warnings emitted. */
|
|
820
|
+
readonly interpretiveCount: number;
|
|
873
821
|
}
|
|
874
822
|
/**
|
|
875
|
-
*
|
|
876
|
-
* that
|
|
877
|
-
* response back to a `ResourceCurrentState<"developerProduct">`. The
|
|
878
|
-
* `update` path consumes the upstream `204 No Content` response and
|
|
879
|
-
* synthesizes the post-update `ResourceCurrentState` from `desired` plus
|
|
880
|
-
* the existing `current.outputs`, carrying `iconImageAssetId` forward when
|
|
881
|
-
* present.
|
|
823
|
+
* Discriminated union describing one observation the migrator made about a
|
|
824
|
+
* Mantle field that did not flow straight into bedrock config or state.
|
|
882
825
|
*
|
|
883
|
-
*
|
|
826
|
+
* - `deferred` - bedrock plans to support the field once the matching
|
|
827
|
+
* resource kind ships; the migration is non-destructive.
|
|
828
|
+
* - `blocked` - no Open Cloud writable endpoint exists; Mantle was using a
|
|
829
|
+
* cookie or legacy API that bedrock cannot call.
|
|
830
|
+
* - `interpretive` - the migrator applied a documented mapping rule
|
|
831
|
+
* (cross-field fold, list-to-flag rewrite, URL-domain dispatch). Each
|
|
832
|
+
* rule names the bedrock-side path it produced and the rule it followed
|
|
833
|
+
* so the user can audit.
|
|
834
|
+
* - `ambiguous` - the field is mappable but unsafe to act on without
|
|
835
|
+
* user input; the migrator carries the hint forward instead of guessing.
|
|
884
836
|
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
837
|
+
* Every variant carries `mantlePath` rooted at the environment so the
|
|
838
|
+
* report is searchable (for example
|
|
839
|
+
* `production.experienceConfiguration_singleton.genre`).
|
|
840
|
+
*/
|
|
841
|
+
type MigrationWarning = {
|
|
842
|
+
readonly bedrockPath: string;
|
|
843
|
+
readonly kind: "interpretive";
|
|
844
|
+
readonly mantlePath: string;
|
|
845
|
+
readonly rule: string;
|
|
846
|
+
} | {
|
|
847
|
+
readonly hint: string;
|
|
848
|
+
readonly kind: "ambiguous";
|
|
849
|
+
readonly mantlePath: string;
|
|
850
|
+
} | {
|
|
851
|
+
readonly kind: "blocked";
|
|
852
|
+
readonly mantlePath: string;
|
|
853
|
+
readonly reason: string;
|
|
854
|
+
} | {
|
|
855
|
+
readonly kind: "deferred";
|
|
856
|
+
readonly mantlePath: string;
|
|
857
|
+
readonly reason: string;
|
|
858
|
+
};
|
|
859
|
+
/**
|
|
860
|
+
* Failure surfaced by `migrateMantleState`. Plain-data discriminated
|
|
861
|
+
* union; narrow on `kind` rather than using `instanceof`.
|
|
887
862
|
*
|
|
888
|
-
*
|
|
863
|
+
* - `stateFileNotFound` - `deps.readFile` threw with `code: "ENOENT"`;
|
|
864
|
+
* the file does not exist at the supplied path. Permission failures
|
|
865
|
+
* (`EACCES`, `EPERM`) and other I/O errors are re-thrown rather than
|
|
866
|
+
* wrapped here, so callers see the original code on the rejection.
|
|
867
|
+
* - `stateParseFailed` - the YAML parser refused the file's contents.
|
|
868
|
+
* - `unsupportedMantleStateVersion` - the parsed file's `version` field is
|
|
869
|
+
* not one of the values in `supported`. V0.1 supports `"6"` only; older
|
|
870
|
+
* versions need to be upgraded with any recent Mantle release first.
|
|
871
|
+
* - `primaryEnvironmentRequired` - the input has more than one environment
|
|
872
|
+
* and `deps.primaryEnvironment` was not supplied. The migrator refuses
|
|
873
|
+
* to silently pick a winner.
|
|
874
|
+
* - `primaryEnvironmentNotFound` - `deps.primaryEnvironment` does not match
|
|
875
|
+
* any environment in the input.
|
|
876
|
+
* - `internalError` - the migrator's own emitted config failed
|
|
877
|
+
* `validateConfig`; `cause` carries the `ConfigError` so callers can
|
|
878
|
+
* inspect each `validationFailed` issue. Defensive bug catcher that
|
|
879
|
+
* callers should never see in practice.
|
|
880
|
+
*/
|
|
881
|
+
type MigrateError = {
|
|
882
|
+
readonly available: ReadonlyArray<string>;
|
|
883
|
+
readonly kind: "primaryEnvironmentNotFound";
|
|
884
|
+
readonly primary: string;
|
|
885
|
+
} | {
|
|
886
|
+
readonly available: ReadonlyArray<string>;
|
|
887
|
+
readonly kind: "primaryEnvironmentRequired";
|
|
888
|
+
} | {
|
|
889
|
+
readonly cause: ConfigError;
|
|
890
|
+
readonly kind: "internalError";
|
|
891
|
+
readonly reason: string;
|
|
892
|
+
} | {
|
|
893
|
+
readonly found: string;
|
|
894
|
+
readonly kind: "unsupportedMantleStateVersion";
|
|
895
|
+
readonly supported: ReadonlyArray<string>;
|
|
896
|
+
} | {
|
|
897
|
+
readonly kind: "stateFileNotFound";
|
|
898
|
+
readonly path: string;
|
|
899
|
+
} | {
|
|
900
|
+
readonly kind: "stateParseFailed";
|
|
901
|
+
readonly path: string;
|
|
902
|
+
readonly reason: string;
|
|
903
|
+
};
|
|
904
|
+
/**
|
|
905
|
+
* Result returned by a successful `migrateMantleState` call.
|
|
889
906
|
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* import {
|
|
894
|
-
* asResourceKey,
|
|
895
|
-
* asRobloxAssetId,
|
|
896
|
-
* createDeveloperProductDriver,
|
|
897
|
-
* } from "@bedrock-rbx/core";
|
|
907
|
+
* `config` is the bedrock-shape projection of the Mantle state file,
|
|
908
|
+
* already validated against the runtime schema (a failure to validate
|
|
909
|
+
* surfaces as `MigrateError.internalError`, not as a returned report).
|
|
898
910
|
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
* data: {
|
|
903
|
-
* body: {
|
|
904
|
-
* createdTimestamp: "2024-01-15T10:30:00.000Z",
|
|
905
|
-
* description: "Stocks the player up with 1,000 premium gems.",
|
|
906
|
-
* iconImageAssetId: null,
|
|
907
|
-
* isForSale: false,
|
|
908
|
-
* isImmutable: false,
|
|
909
|
-
* name: "Gem Pack",
|
|
910
|
-
* priceInformation: null,
|
|
911
|
-
* productId: 9_876_543_210,
|
|
912
|
-
* storePageEnabled: false,
|
|
913
|
-
* universeId: 1_234_567_890,
|
|
914
|
-
* updatedTimestamp: "2024-01-15T10:30:00.000Z",
|
|
915
|
-
* },
|
|
916
|
-
* headers: {},
|
|
917
|
-
* status: 200,
|
|
918
|
-
* },
|
|
919
|
-
* success: true,
|
|
920
|
-
* };
|
|
921
|
-
* },
|
|
922
|
-
* };
|
|
911
|
+
* `configFileContent` is the same data rendered as TypeScript source
|
|
912
|
+
* (`defineConfig({...})`) so the caller can write it straight to disk
|
|
913
|
+
* without re-serializing. `loadConfig` round-trips it cleanly.
|
|
923
914
|
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
* httpClient,
|
|
928
|
-
* sleep: async () => {},
|
|
929
|
-
* }),
|
|
930
|
-
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
931
|
-
* universeId: asRobloxAssetId("1234567890"),
|
|
932
|
-
* });
|
|
915
|
+
* `statesByEnvironment` carries one in-memory `BedrockState` per
|
|
916
|
+
* environment from the input. Truthful per environment (no factorization)
|
|
917
|
+
* so `bedrock deploy --env=<env>` produces zero ops on first run.
|
|
933
918
|
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
* description: "Stocks the player up with 1,000 premium gems.",
|
|
937
|
-
* isRegionalPricingEnabled: undefined,
|
|
938
|
-
* key: asResourceKey("gem-pack"),
|
|
939
|
-
* kind: "developerProduct",
|
|
940
|
-
* name: "Gem Pack",
|
|
941
|
-
* price: undefined,
|
|
942
|
-
* storePageEnabled: undefined,
|
|
943
|
-
* })
|
|
944
|
-
* .then((result) => {
|
|
945
|
-
* expect(result.success).toBeTrue();
|
|
946
|
-
* if (result.success) {
|
|
947
|
-
* expect(result.data.outputs.productId).toBe("9876543210");
|
|
948
|
-
* }
|
|
949
|
-
* });
|
|
950
|
-
* ```
|
|
919
|
+
* `warnings` and `summary` describe what the migrator did *not* migrate
|
|
920
|
+
* verbatim, classified for triage. The skeleton emits no warnings.
|
|
951
921
|
*/
|
|
952
|
-
|
|
922
|
+
interface MigrationReport {
|
|
923
|
+
/** Validated bedrock config built from the Mantle state file. */
|
|
924
|
+
readonly config: Config;
|
|
925
|
+
/** Same `config` rendered as TypeScript source the caller can write to disk. */
|
|
926
|
+
readonly configFileContent: string;
|
|
927
|
+
/** One `BedrockState` per environment in the input, keyed by environment name. */
|
|
928
|
+
readonly statesByEnvironment: StatesByEnvironment;
|
|
929
|
+
/** Aggregate counts of `warnings` by kind. */
|
|
930
|
+
readonly summary: MigrationSummary;
|
|
931
|
+
/** One entry per non-trivial mapping or skipped Mantle field. */
|
|
932
|
+
readonly warnings: ReadonlyArray<MigrationWarning>;
|
|
933
|
+
}
|
|
953
934
|
//#endregion
|
|
954
|
-
//#region src/
|
|
935
|
+
//#region src/ports/state-port.d.ts
|
|
955
936
|
/**
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
959
|
-
*
|
|
937
|
+
* Plugin contract for persisting deployment state: the interface an adapter
|
|
938
|
+
* (Gist, local filesystem, cloud object store) implements to let Bedrock load
|
|
939
|
+
* and save its per-environment {@link BedrockState} snapshot.
|
|
940
|
+
*
|
|
941
|
+
* `StatePort` is a *driven* (secondary) port in hexagonal terms, following the
|
|
942
|
+
* same naming convention as {@link "./resource-driver".ResourceDriver}.
|
|
960
943
|
*
|
|
961
944
|
* @example
|
|
962
945
|
*
|
|
963
946
|
* ```ts
|
|
964
|
-
* import type {
|
|
965
|
-
* import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
|
|
966
|
-
* import { asRobloxAssetId, type GamePassDriverDeps } from "@bedrock-rbx/core";
|
|
947
|
+
* import type { BedrockState, StatePort } from "@bedrock-rbx/core";
|
|
967
948
|
*
|
|
968
|
-
* const
|
|
969
|
-
* async request() {
|
|
970
|
-
* return { data: { body: {}, headers: {}, status: 200 }, success: true };
|
|
971
|
-
* },
|
|
972
|
-
* };
|
|
949
|
+
* const store = new Map<string, BedrockState>();
|
|
973
950
|
*
|
|
974
|
-
* const
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
*
|
|
951
|
+
* const statePort: StatePort = {
|
|
952
|
+
* async read(environment) {
|
|
953
|
+
* return { data: store.get(environment), success: true };
|
|
954
|
+
* },
|
|
955
|
+
* async write(state) {
|
|
956
|
+
* store.set(state.environment, state);
|
|
957
|
+
* return { data: undefined, success: true };
|
|
958
|
+
* },
|
|
982
959
|
* };
|
|
983
960
|
*
|
|
984
|
-
*
|
|
961
|
+
* return statePort
|
|
962
|
+
* .read("production")
|
|
963
|
+
* .then((firstRead) => {
|
|
964
|
+
* expect(firstRead.success).toBeTrue();
|
|
965
|
+
* if (firstRead.success) {
|
|
966
|
+
* expect(firstRead.data).toBeUndefined();
|
|
967
|
+
* }
|
|
968
|
+
* return statePort.write({
|
|
969
|
+
* environment: "production",
|
|
970
|
+
* resources: [],
|
|
971
|
+
* version: 1,
|
|
972
|
+
* });
|
|
973
|
+
* })
|
|
974
|
+
* .then((writeResult) => {
|
|
975
|
+
* expect(writeResult.success).toBeTrue();
|
|
976
|
+
* return statePort.read("production");
|
|
977
|
+
* })
|
|
978
|
+
* .then((secondRead) => {
|
|
979
|
+
* expect(secondRead.success).toBeTrue();
|
|
980
|
+
* if (secondRead.success && secondRead.data !== undefined) {
|
|
981
|
+
* expect(secondRead.data.environment).toBe("production");
|
|
982
|
+
* expect(secondRead.data.resources).toBeEmpty();
|
|
983
|
+
* }
|
|
984
|
+
* });
|
|
985
985
|
* ```
|
|
986
986
|
*/
|
|
987
|
-
interface
|
|
988
|
-
/**
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
987
|
+
interface StatePort {
|
|
988
|
+
/**
|
|
989
|
+
* Reads state for the given environment.
|
|
990
|
+
*
|
|
991
|
+
* - Returns `Ok(undefined)` when no state file exists (legitimate first deploy).
|
|
992
|
+
* - Returns `Err(StateError)` when a file exists but cannot be parsed
|
|
993
|
+
* (corrupt JSON, schema failure, unknown `$bedrock.version`).
|
|
994
|
+
*
|
|
995
|
+
* Never silently falls back to empty state: a malformed file that collapsed
|
|
996
|
+
* to `{ resources: [] }` would cause the next apply to re-create every
|
|
997
|
+
* resource on Roblox.
|
|
998
|
+
*/
|
|
999
|
+
read(environment: string): Promise<Result$1<BedrockState | undefined, StateError>>;
|
|
1000
|
+
/** Writes state for the given environment, overwriting any existing file. */
|
|
1001
|
+
write(state: BedrockState): Promise<Result$1<void, StateError>>;
|
|
994
1002
|
}
|
|
1003
|
+
//#endregion
|
|
1004
|
+
//#region src/adapters/gist-state-adapter.d.ts
|
|
995
1005
|
/**
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
* `
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
*
|
|
1003
|
-
|
|
1006
|
+
* Minimal `fetch`-compatible signature the adapter needs, narrower than
|
|
1007
|
+
* `typeof globalThis.fetch` so test fakes do not have to stub runtime
|
|
1008
|
+
* extensions such as `fetch.preconnect`.
|
|
1009
|
+
*/
|
|
1010
|
+
type GistFetch = (input: globalThis.Request | string | URL, init?: RequestInit) => Promise<Response>;
|
|
1011
|
+
/**
|
|
1012
|
+
* Configuration for {@link createGistStateAdapter}.
|
|
1013
|
+
*/
|
|
1014
|
+
interface GistStateAdapterDeps {
|
|
1015
|
+
/** Injection seam for tests; defaults to `globalThis.fetch`. */
|
|
1016
|
+
readonly fetch?: GistFetch | undefined;
|
|
1017
|
+
/** ID of an existing GitHub Gist that holds this project's state files. */
|
|
1018
|
+
readonly gistId: string;
|
|
1019
|
+
/**
|
|
1020
|
+
* Injection seam for retry backoff timing; defaults to a `setTimeout`-based
|
|
1021
|
+
* promise. Tests pass a fake to keep retry assertions deterministic.
|
|
1022
|
+
*/
|
|
1023
|
+
readonly sleep?: ((ms: number) => Promise<void>) | undefined;
|
|
1024
|
+
/** GitHub token (fine-grained PAT or classic PAT) with gist read/write scope. */
|
|
1025
|
+
readonly token: string;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Build a `StatePort` that persists Bedrock state in a GitHub Gist.
|
|
1004
1029
|
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1030
|
+
* One gist holds one file per environment, named `state.<env>.json`. The
|
|
1031
|
+
* adapter authenticates with a user-supplied token and speaks the GitHub
|
|
1032
|
+
* REST API directly; no SDK dependency.
|
|
1008
1033
|
*
|
|
1009
1034
|
* @example
|
|
1010
1035
|
*
|
|
1011
1036
|
* ```ts
|
|
1012
|
-
* import
|
|
1013
|
-
* import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
|
|
1014
|
-
* import {
|
|
1015
|
-
* asResourceKey,
|
|
1016
|
-
* asRobloxAssetId,
|
|
1017
|
-
* asSha256Hex,
|
|
1018
|
-
* createGamePassDriver,
|
|
1019
|
-
* } from "@bedrock-rbx/core";
|
|
1020
|
-
*
|
|
1021
|
-
* const httpClient: HttpClient = {
|
|
1022
|
-
* async request() {
|
|
1023
|
-
* return {
|
|
1024
|
-
* data: {
|
|
1025
|
-
* body: {
|
|
1026
|
-
* createdTimestamp: "2024-01-15T10:30:00.000Z",
|
|
1027
|
-
* description: "Grants VIP perks.",
|
|
1028
|
-
* gamePassId: 9_876_543_210,
|
|
1029
|
-
* iconAssetId: 1_122_334_455,
|
|
1030
|
-
* isForSale: true,
|
|
1031
|
-
* name: "VIP Pass",
|
|
1032
|
-
* updatedTimestamp: "2024-01-15T10:30:00.000Z",
|
|
1033
|
-
* },
|
|
1034
|
-
* headers: {},
|
|
1035
|
-
* status: 200,
|
|
1036
|
-
* },
|
|
1037
|
-
* success: true,
|
|
1038
|
-
* };
|
|
1039
|
-
* },
|
|
1040
|
-
* };
|
|
1037
|
+
* import { createGistStateAdapter } from "@bedrock-rbx/core";
|
|
1041
1038
|
*
|
|
1042
|
-
* const
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
* }),
|
|
1048
|
-
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
1049
|
-
* universeId: asRobloxAssetId("1234567890"),
|
|
1039
|
+
* const port = createGistStateAdapter({
|
|
1040
|
+
* fetch: async () =>
|
|
1041
|
+
* new Response(JSON.stringify({ files: {} }), { status: 200 }),
|
|
1042
|
+
* gistId: "abc123def456",
|
|
1043
|
+
* token: "ghp_example",
|
|
1050
1044
|
* });
|
|
1051
1045
|
*
|
|
1052
|
-
* return
|
|
1053
|
-
* .
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1059
|
-
* ),
|
|
1060
|
-
* },
|
|
1061
|
-
* key: asResourceKey("vip-pass"),
|
|
1062
|
-
* kind: "gamePass",
|
|
1063
|
-
* name: "VIP Pass",
|
|
1064
|
-
* price: 500,
|
|
1065
|
-
* })
|
|
1066
|
-
* .then((result) => {
|
|
1067
|
-
* expect(result.success).toBeTrue();
|
|
1068
|
-
* if (result.success) {
|
|
1069
|
-
* expect(result.data.outputs.assetId).toBe("9876543210");
|
|
1070
|
-
* }
|
|
1071
|
-
* });
|
|
1046
|
+
* return port.read("production").then((result) => {
|
|
1047
|
+
* expect(result.success).toBeTrue();
|
|
1048
|
+
* if (result.success) {
|
|
1049
|
+
* expect(result.data).toBeUndefined();
|
|
1050
|
+
* }
|
|
1051
|
+
* });
|
|
1072
1052
|
* ```
|
|
1053
|
+
*
|
|
1054
|
+
* @param deps - Gist ID, GitHub token, and optional fetch override.
|
|
1055
|
+
* @returns A `StatePort` ready to be passed to `deploy()`.
|
|
1073
1056
|
*/
|
|
1074
|
-
declare function
|
|
1057
|
+
declare function createGistStateAdapter(deps: GistStateAdapterDeps): StatePort;
|
|
1075
1058
|
//#endregion
|
|
1076
|
-
//#region src/
|
|
1059
|
+
//#region src/shell/build-state-port.d.ts
|
|
1077
1060
|
/**
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1061
|
+
* Failure surfaced when a default-constructed adapter cannot find a
|
|
1062
|
+
* required environment variable. The deploy boundary wraps this in a
|
|
1063
|
+
* `DeployError` so the caller sees a typed Result instead of an
|
|
1064
|
+
* exception or a confusing downstream HTTP error.
|
|
1065
|
+
*/
|
|
1066
|
+
interface MissingCredentialError {
|
|
1067
|
+
/** Literal discriminator for narrowing. */
|
|
1068
|
+
readonly kind: "missingCredential";
|
|
1069
|
+
/** Whether the credential was needed for the state backend or the driver registry. */
|
|
1070
|
+
readonly purpose: "registry" | "stateBackend";
|
|
1071
|
+
/** Environment variable name the default-construction path tried to read. */
|
|
1072
|
+
readonly variable: string;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Failure surfaced when the dispatch helper sees a `state.backend` value
|
|
1076
|
+
* it does not recognize. The hint points at `opts.statePort` so the
|
|
1077
|
+
* caller can pass a custom adapter as an escape hatch.
|
|
1078
|
+
*/
|
|
1079
|
+
interface UnsupportedBackendError {
|
|
1080
|
+
/** Backend name read from `state.backend`. */
|
|
1081
|
+
readonly backend: string;
|
|
1082
|
+
/** Suggested escape hatch routed back to the caller. */
|
|
1083
|
+
readonly hint: string;
|
|
1084
|
+
/** Literal discriminator for narrowing. */
|
|
1085
|
+
readonly kind: "unsupportedBackend";
|
|
1086
|
+
}
|
|
1087
|
+
/** Inputs for {@link buildStatePort}. */
|
|
1088
|
+
interface BuildStatePortDeps {
|
|
1089
|
+
/** Optional `fetch` seam plumbed through to the gist adapter for tests. */
|
|
1090
|
+
readonly fetch?: GistFetch | undefined;
|
|
1091
|
+
/** Reads an environment variable; injected so tests stay free of `process.env`. */
|
|
1092
|
+
readonly getEnv: (name: string) => string | undefined;
|
|
1093
|
+
/** Resolved state configuration for the target environment. */
|
|
1094
|
+
readonly stateConfig: StateConfig;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Construct a `StatePort` from a resolved `StateConfig`. Dispatches on
|
|
1098
|
+
* `stateConfig.backend` to the matching builtin adapter; reads the
|
|
1099
|
+
* required credential from `getEnv` and surfaces `missingCredential` or
|
|
1100
|
+
* `unsupportedBackend` as typed Results.
|
|
1086
1101
|
*
|
|
1087
1102
|
* @example
|
|
1088
1103
|
*
|
|
1089
1104
|
* ```ts
|
|
1090
|
-
* import {
|
|
1091
|
-
* asResourceKey,
|
|
1092
|
-
* asRobloxAssetId,
|
|
1093
|
-
* asSha256Hex,
|
|
1094
|
-
* type BedrockState,
|
|
1095
|
-
* } from "@bedrock-rbx/core";
|
|
1105
|
+
* import { buildStatePort } from "@bedrock-rbx/core";
|
|
1096
1106
|
*
|
|
1097
|
-
* const
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
* iconFileHashes: {
|
|
1104
|
-
* "en-us": asSha256Hex(
|
|
1105
|
-
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1106
|
-
* ),
|
|
1107
|
-
* },
|
|
1108
|
-
* key: asResourceKey("vip-pass"),
|
|
1109
|
-
* kind: "gamePass",
|
|
1110
|
-
* name: "VIP Pass",
|
|
1111
|
-
* outputs: {
|
|
1112
|
-
* assetId: asRobloxAssetId("9876543210"),
|
|
1113
|
-
* iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
|
|
1114
|
-
* },
|
|
1115
|
-
* price: 500,
|
|
1116
|
-
* },
|
|
1117
|
-
* ],
|
|
1118
|
-
* version: 1,
|
|
1119
|
-
* };
|
|
1107
|
+
* const port = buildStatePort({
|
|
1108
|
+
* fetch: async () =>
|
|
1109
|
+
* new Response(JSON.stringify({ files: {} }), { status: 200 }),
|
|
1110
|
+
* getEnv: (name) => (name === "GITHUB_TOKEN" ? "ghp_example" : undefined),
|
|
1111
|
+
* stateConfig: { backend: "gist", gistId: "abc123" },
|
|
1112
|
+
* });
|
|
1120
1113
|
*
|
|
1121
|
-
* expect(
|
|
1122
|
-
* expect(state.resources).toHaveLength(1);
|
|
1114
|
+
* expect(port.success).toBeTrue();
|
|
1123
1115
|
* ```
|
|
1116
|
+
*
|
|
1117
|
+
* @param deps - Resolved state config plus credential-injection seams.
|
|
1118
|
+
* @returns A `StatePort` on success, or a typed Err describing the
|
|
1119
|
+
* missing credential or the unsupported backend.
|
|
1124
1120
|
*/
|
|
1125
|
-
|
|
1126
|
-
|
|
1121
|
+
declare function buildStatePort(deps: BuildStatePortDeps): Result$1<StatePort, MissingCredentialError | UnsupportedBackendError>;
|
|
1122
|
+
//#endregion
|
|
1123
|
+
//#region src/core/resolve-state-config.d.ts
|
|
1124
|
+
/**
|
|
1125
|
+
* Failure surfaced when no `StateConfig` is configured for the requested
|
|
1126
|
+
* environment. The shell layer wraps this in a `DeployError` when default
|
|
1127
|
+
* state-port construction is requested but the project has not declared
|
|
1128
|
+
* where state should live.
|
|
1129
|
+
*/
|
|
1130
|
+
interface StateNotConfiguredError {
|
|
1131
|
+
/** Environment that the resolver was called against. */
|
|
1127
1132
|
readonly environment: string;
|
|
1128
|
-
/**
|
|
1129
|
-
readonly
|
|
1130
|
-
/** Schema-version literal; bumped only for breaking changes to the on-disk format. */
|
|
1131
|
-
readonly version: 1;
|
|
1133
|
+
/** Literal discriminator for narrowing. */
|
|
1134
|
+
readonly kind: "stateNotConfigured";
|
|
1132
1135
|
}
|
|
1133
1136
|
/**
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
|
|
1137
|
+
* Minimal structural input the state resolver needs. Both `Config`
|
|
1138
|
+
* (pre-merge, discriminated XOR union) and `ResolvedConfig` (post-merge)
|
|
1139
|
+
* satisfy this shape, so callers can route either in without coupling
|
|
1140
|
+
* the resolver to the discriminated-union arms.
|
|
1141
|
+
*/
|
|
1142
|
+
interface StateResolutionInputs {
|
|
1143
|
+
readonly environments: Record<string, undefined | {
|
|
1144
|
+
readonly state?: StateConfig;
|
|
1145
|
+
}>;
|
|
1146
|
+
readonly state?: StateConfig;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Pick the `StateConfig` that applies to `environment`. Per-environment
|
|
1150
|
+
* overrides win over the root block; if neither is present, returns
|
|
1151
|
+
* `Err(stateNotConfigured)` so the deploy boundary can surface a typed
|
|
1152
|
+
* error instead of silently falling back.
|
|
1139
1153
|
*
|
|
1154
|
+
* @param config - Validated project config.
|
|
1155
|
+
* @param environment - Target environment name.
|
|
1156
|
+
* @returns The resolved `StateConfig`, or `Err(stateNotConfigured)` when
|
|
1157
|
+
* neither the environment override nor the root block is set.
|
|
1140
1158
|
* @example
|
|
1141
1159
|
*
|
|
1142
1160
|
* ```ts
|
|
1143
|
-
* import
|
|
1161
|
+
* import { resolveStateConfig } from "@bedrock-rbx/core";
|
|
1144
1162
|
*
|
|
1145
|
-
* const
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
* }
|
|
1163
|
+
* const result = resolveStateConfig(
|
|
1164
|
+
* {
|
|
1165
|
+
* state: { backend: "gist", gistId: "root-gist" },
|
|
1166
|
+
* environments: {
|
|
1167
|
+
* production: { state: { backend: "gist", gistId: "prod-gist" } },
|
|
1168
|
+
* },
|
|
1169
|
+
* },
|
|
1170
|
+
* "production",
|
|
1171
|
+
* );
|
|
1150
1172
|
*
|
|
1151
|
-
* expect(
|
|
1173
|
+
* expect(result.success).toBeTrue();
|
|
1174
|
+
* if (result.success) {
|
|
1175
|
+
* expect(result.data).toContainEntry(["gistId", "prod-gist"]);
|
|
1176
|
+
* }
|
|
1152
1177
|
* ```
|
|
1153
1178
|
*/
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1179
|
+
declare function resolveStateConfig(config: StateResolutionInputs, environment: string): Result$1<StateConfig, StateNotConfiguredError>;
|
|
1180
|
+
//#endregion
|
|
1181
|
+
//#region src/core/select-environment.d.ts
|
|
1182
|
+
/**
|
|
1183
|
+
* Failure surfaced when `selectEnvironment` is asked for an environment
|
|
1184
|
+
* name that is not a key of `config.environments`. Carries the list of
|
|
1185
|
+
* declared names so callers can render a "did you mean?" hint or a
|
|
1186
|
+
* close-match suggestion.
|
|
1187
|
+
*/
|
|
1188
|
+
interface UnknownEnvironmentError {
|
|
1189
|
+
/** Environment names that the config actually declared. */
|
|
1190
|
+
readonly declared: ReadonlyArray<string>;
|
|
1191
|
+
/** Environment name the caller asked for. */
|
|
1192
|
+
readonly environment: string;
|
|
1157
1193
|
/** Literal discriminator for narrowing. */
|
|
1158
|
-
readonly kind: "
|
|
1159
|
-
/** Human-readable explanation of why the file could not be trusted. */
|
|
1160
|
-
readonly reason: string;
|
|
1194
|
+
readonly kind: "unknownEnvironment";
|
|
1161
1195
|
}
|
|
1162
|
-
//#endregion
|
|
1163
|
-
//#region src/ports/state-port.d.ts
|
|
1164
1196
|
/**
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
* `
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
* @example
|
|
1173
|
-
*
|
|
1174
|
-
* ```ts
|
|
1175
|
-
* import type { BedrockState, StatePort } from "@bedrock-rbx/core";
|
|
1176
|
-
*
|
|
1177
|
-
* const store = new Map<string, BedrockState>();
|
|
1178
|
-
*
|
|
1179
|
-
* const statePort: StatePort = {
|
|
1180
|
-
* async read(environment) {
|
|
1181
|
-
* return { data: store.get(environment), success: true };
|
|
1182
|
-
* },
|
|
1183
|
-
* async write(state) {
|
|
1184
|
-
* store.set(state.environment, state);
|
|
1185
|
-
* return { data: undefined, success: true };
|
|
1186
|
-
* },
|
|
1187
|
-
* };
|
|
1188
|
-
*
|
|
1189
|
-
* return statePort
|
|
1190
|
-
* .read("production")
|
|
1191
|
-
* .then((firstRead) => {
|
|
1192
|
-
* expect(firstRead.success).toBeTrue();
|
|
1193
|
-
* if (firstRead.success) {
|
|
1194
|
-
* expect(firstRead.data).toBeUndefined();
|
|
1195
|
-
* }
|
|
1196
|
-
* return statePort.write({
|
|
1197
|
-
* environment: "production",
|
|
1198
|
-
* resources: [],
|
|
1199
|
-
* version: 1,
|
|
1200
|
-
* });
|
|
1201
|
-
* })
|
|
1202
|
-
* .then((writeResult) => {
|
|
1203
|
-
* expect(writeResult.success).toBeTrue();
|
|
1204
|
-
* return statePort.read("production");
|
|
1205
|
-
* })
|
|
1206
|
-
* .then((secondRead) => {
|
|
1207
|
-
* expect(secondRead.success).toBeTrue();
|
|
1208
|
-
* if (secondRead.success && secondRead.data !== undefined) {
|
|
1209
|
-
* expect(secondRead.data.environment).toBe("production");
|
|
1210
|
-
* expect(secondRead.data.resources).toBeEmpty();
|
|
1211
|
-
* }
|
|
1212
|
-
* });
|
|
1213
|
-
* ```
|
|
1197
|
+
* Failure surfaced when a merged place entry is missing a required field.
|
|
1198
|
+
* Two paths reach this error: a root place declared without a matching
|
|
1199
|
+
* per-environment overlay supplying `placeId`, and an overlay-only place
|
|
1200
|
+
* declared under `environments.X.places` with no matching root entry to
|
|
1201
|
+
* supply `filePath`. Surfacing both at the resolution boundary attributes
|
|
1202
|
+
* the missing field to the offending entry's key instead of letting
|
|
1203
|
+
* `buildDesired` crash with a generic `fileReadFailed` later on.
|
|
1214
1204
|
*/
|
|
1215
|
-
interface
|
|
1216
|
-
/**
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
* to `{ resources: [] }` would cause the next apply to re-create every
|
|
1225
|
-
* resource on Roblox.
|
|
1226
|
-
*/
|
|
1227
|
-
read(environment: string): Promise<Result$1<BedrockState | undefined, StateError>>;
|
|
1228
|
-
/** Writes state for the given environment, overwriting any existing file. */
|
|
1229
|
-
write(state: BedrockState): Promise<Result$1<void, StateError>>;
|
|
1205
|
+
interface IncompletePlaceEntryError {
|
|
1206
|
+
/** ResourceKey of the place entry that is missing a required field. */
|
|
1207
|
+
readonly key: string;
|
|
1208
|
+
/** Environment whose overlay was projected onto the config. */
|
|
1209
|
+
readonly environment: string;
|
|
1210
|
+
/** Literal discriminator for narrowing. */
|
|
1211
|
+
readonly kind: "incompletePlaceEntry";
|
|
1212
|
+
/** Field that the merged entry lacks. */
|
|
1213
|
+
readonly missingField: "filePath" | "placeId";
|
|
1230
1214
|
}
|
|
1231
|
-
//#endregion
|
|
1232
|
-
//#region src/adapters/gist-state-adapter.d.ts
|
|
1233
1215
|
/**
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1216
|
+
* Failure surfaced when a merged `universe` block lacks `universeId`.
|
|
1217
|
+
* The schema-level XOR rule normally prevents this by requiring
|
|
1218
|
+
* `universeId` either at the root or on every per-environment overlay;
|
|
1219
|
+
* this error remains as a typed safety net for callers that bypass
|
|
1220
|
+
* `validateConfig` and hand a `Config` to `selectEnvironment` directly.
|
|
1237
1221
|
*/
|
|
1238
|
-
|
|
1222
|
+
interface IncompleteUniverseEntryError {
|
|
1223
|
+
/** Environment whose overlay was projected onto the config. */
|
|
1224
|
+
readonly environment: string;
|
|
1225
|
+
/** Literal discriminator for narrowing. */
|
|
1226
|
+
readonly kind: "incompleteUniverseEntry";
|
|
1227
|
+
/** Field that the merged entry lacks. V1 only surfaces `"universeId"`. */
|
|
1228
|
+
readonly missingField: "universeId";
|
|
1229
|
+
}
|
|
1239
1230
|
/**
|
|
1240
|
-
*
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1231
|
+
* Failure surfaced when a merged `passes` entry is missing a required
|
|
1232
|
+
* field. The most common path here is an overlay-only pass declared
|
|
1233
|
+
* under `environments.X.passes` with no matching root entry: the overlay
|
|
1234
|
+
* shape is `Partial<GamePassEntry>`, so a typo on the ResourceKey
|
|
1235
|
+
* silently produces an incomplete entry that would otherwise be filled
|
|
1236
|
+
* in by `applyRedaction` (when `redacted: true` is set) and pushed as a
|
|
1237
|
+
* phantom placeholder pass. Surfacing the missing field at the
|
|
1238
|
+
* resolution boundary keeps that case attributable instead of letting
|
|
1239
|
+
* normalize fail later with a less specific error.
|
|
1240
|
+
*/
|
|
1241
|
+
interface IncompletePassEntryError {
|
|
1242
|
+
/** ResourceKey of the pass entry that is missing a required field. */
|
|
1243
|
+
readonly key: string;
|
|
1244
|
+
/** Environment whose overlay was projected onto the config. */
|
|
1245
|
+
readonly environment: string;
|
|
1246
|
+
/** Literal discriminator for narrowing. */
|
|
1247
|
+
readonly kind: "incompletePassEntry";
|
|
1248
|
+
/** Field that the merged entry lacks. */
|
|
1249
|
+
readonly missingField: "description" | "icon" | "name";
|
|
1254
1250
|
}
|
|
1251
|
+
/** Failure modes returned by {@link selectEnvironment}. */
|
|
1252
|
+
type SelectEnvironmentError = IncompletePassEntryError | IncompletePlaceEntryError | IncompleteUniverseEntryError | UnknownEnvironmentError;
|
|
1255
1253
|
/**
|
|
1256
|
-
*
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
1254
|
+
* Project a validated `Config` onto a single environment. Looks up the
|
|
1255
|
+
* matching `environments[environment]` entry, deep-merges its resource
|
|
1256
|
+
* overlay (`passes`, `places`, `universe`) over the root config via defu,
|
|
1257
|
+
* and applies the env-level state override when present (the env entry's
|
|
1258
|
+
* `state` field wins; otherwise the root `state` flows through).
|
|
1261
1259
|
*
|
|
1262
|
-
*
|
|
1260
|
+
* Pure: no I/O. Returns a `ResolvedConfig` ready to feed into downstream
|
|
1261
|
+
* functions (`flattenConfig`, `buildDefaultRegistry`, `resolveStateConfig`).
|
|
1262
|
+
* The post-merge view promotes `places` from `Record<string, PlaceEntry>`
|
|
1263
|
+
* (root: file-paths only) to `Record<string, ResolvedPlaceEntry>` (root +
|
|
1264
|
+
* overlay merged). `environments` and `extends` are passed through
|
|
1265
|
+
* unchanged because they preserve the shape relationship to `Config`;
|
|
1266
|
+
* downstream consumers do not read them post-merge.
|
|
1263
1267
|
*
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1268
|
+
* Defu's merge semantics are deliberate: keyed-map collections merge by
|
|
1269
|
+
* key (so a place declared in both root and overlay produces a single
|
|
1270
|
+
* entry whose overlay-supplied fields win), and `null` / `undefined` in
|
|
1271
|
+
* the overlay are skipped (so the overlay never deletes a root field).
|
|
1272
|
+
* State has its own resolution path (a single replacement, not a
|
|
1273
|
+
* deep-merge) because it is a tagged union: a deep-merge of
|
|
1274
|
+
* `{ backend: "s3" }` over `{ backend: "gist", gistId }` would produce
|
|
1275
|
+
* a malformed `{ backend: "s3", gistId }`.
|
|
1266
1276
|
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
* token: "ghp_example",
|
|
1272
|
-
* });
|
|
1277
|
+
* State is left absent when neither the env override nor the root block
|
|
1278
|
+
* provides one. Callers that require a resolved `StateConfig` should
|
|
1279
|
+
* route through `resolveStateConfig` or `buildStatePort`; the absent
|
|
1280
|
+
* case surfaces as a typed `stateNotConfigured` there.
|
|
1273
1281
|
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1282
|
+
* Limitation in v1: a per-environment universe overlay that introduces a
|
|
1283
|
+
* brand-new universe block may still have optional fields missing, since
|
|
1284
|
+
* the overlay type only requires the identity-bearing key. The resolver
|
|
1285
|
+
* surfaces the entry as-is; the universe driver reports the missing
|
|
1286
|
+
* field when it tries to consume the entry. Universe is a singleton with
|
|
1287
|
+
* 20+ optional fields, so the same `incompletePlaceEntry`-style validation
|
|
1288
|
+
* is deferred to a separate follow-up.
|
|
1281
1289
|
*
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
* Dependencies of `createPlaceDriver`. `universeId` is captured at
|
|
1290
|
-
* construction time (matching `GamePassDriverDeps`) so each driver instance
|
|
1291
|
-
* is bound to a single universe; multi-universe deploys construct one driver
|
|
1292
|
-
* per universe. `readFile` is injected because `diff` operates on file hashes
|
|
1293
|
-
* while the driver is the only place that needs the raw bytes.
|
|
1290
|
+
* When the project sets a `displayNamePrefix` (or omits it, in which case
|
|
1291
|
+
* prefixing defaults to enabled) and the chosen environment declares a
|
|
1292
|
+
* non-empty `label`, the resolver renders the configured template via
|
|
1293
|
+
* `renderDisplayNamePrefix` and prepends the result to `universe.displayName`
|
|
1294
|
+
* and every declared place `displayName`. An undeclared `displayName`, an
|
|
1295
|
+
* empty/absent label, or an explicit `displayNamePrefix.enabled: false` all
|
|
1296
|
+
* skip prefixing for the affected fields.
|
|
1294
1297
|
*
|
|
1295
1298
|
* @example
|
|
1296
1299
|
*
|
|
1297
1300
|
* ```ts
|
|
1298
|
-
* import
|
|
1299
|
-
* import {
|
|
1300
|
-
* import { asRobloxAssetId, type PlaceDriverDeps } from "@bedrock-rbx/core";
|
|
1301
|
+
* import { selectEnvironment } from "@bedrock-rbx/core";
|
|
1302
|
+
* import type { Config } from "@bedrock-rbx/core/config";
|
|
1301
1303
|
*
|
|
1302
|
-
* const
|
|
1303
|
-
*
|
|
1304
|
-
*
|
|
1304
|
+
* const config: Config = {
|
|
1305
|
+
* environments: {
|
|
1306
|
+
* production: { universe: { universeId: "999" } },
|
|
1305
1307
|
* },
|
|
1308
|
+
* state: { backend: "gist", gistId: "abc123" },
|
|
1309
|
+
* universe: { voiceChatEnabled: true },
|
|
1306
1310
|
* };
|
|
1307
1311
|
*
|
|
1308
|
-
* const
|
|
1309
|
-
* client: new PlacesClient({
|
|
1310
|
-
* apiKey: "rbx-your-key",
|
|
1311
|
-
* httpClient,
|
|
1312
|
-
* sleep: async () => {},
|
|
1313
|
-
* }),
|
|
1314
|
-
* readFile: async () => new Uint8Array(),
|
|
1315
|
-
* universeId: asRobloxAssetId("1234567890"),
|
|
1316
|
-
* };
|
|
1312
|
+
* const result = selectEnvironment(config, "production");
|
|
1317
1313
|
*
|
|
1318
|
-
* expect(
|
|
1314
|
+
* expect(result.success).toBeTrue();
|
|
1315
|
+
* if (result.success) {
|
|
1316
|
+
* expect(result.data.universe?.universeId).toBe("999");
|
|
1317
|
+
* expect(result.data.universe?.voiceChatEnabled).toBeTrue();
|
|
1318
|
+
* expect(result.data.state?.backend).toBe("gist");
|
|
1319
|
+
* }
|
|
1319
1320
|
* ```
|
|
1321
|
+
*
|
|
1322
|
+
* @param config - Validated project config carrying at least one
|
|
1323
|
+
* environment under `environments`.
|
|
1324
|
+
* @param environment - Environment name to project onto. Must be a key
|
|
1325
|
+
* of `config.environments`.
|
|
1326
|
+
* @returns `Ok(ResolvedConfig)` with the merged resource fields and the
|
|
1327
|
+
* resolved state, or `Err(SelectEnvironmentError)` describing why the
|
|
1328
|
+
* projection failed.
|
|
1320
1329
|
*/
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
/** Reads place-file bytes for upload; rejections propagate out of the driver. */
|
|
1325
|
-
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
1326
|
-
/** Universe that owns every place this driver publishes. */
|
|
1327
|
-
readonly universeId: RobloxAssetId;
|
|
1328
|
-
}
|
|
1330
|
+
declare function selectEnvironment(config: Config, environment: string): Result$1<ResolvedConfig, SelectEnvironmentError>;
|
|
1331
|
+
//#endregion
|
|
1332
|
+
//#region src/ports/resource-driver.d.ts
|
|
1329
1333
|
/**
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
* upstream
|
|
1333
|
-
* place" endpoint (the place is user-supplied input), only "publish version".
|
|
1334
|
+
* Plugin contract for a resource adapter: the interface a third-party author
|
|
1335
|
+
* implements to teach Bedrock how to reconcile one {@link ResourceKind} against
|
|
1336
|
+
* its upstream API.
|
|
1334
1337
|
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
+
* `ResourceDriver<K>` is a *driven* (secondary) port in hexagonal terms; the
|
|
1339
|
+
* name "driver" follows Terraform, Pulumi, and Mantle IaC convention for a
|
|
1340
|
+
* component that talks to a specific resource API.
|
|
1338
1341
|
*
|
|
1339
|
-
* @
|
|
1340
|
-
* @returns A driver indexable by `"place"` in a `DriverRegistry`.
|
|
1341
|
-
* @throws Whatever `deps.readFile` rejects with.
|
|
1342
|
+
* @template K - The {@link ResourceKind} discriminator this driver handles.
|
|
1342
1343
|
*
|
|
1343
1344
|
* @example
|
|
1344
1345
|
*
|
|
1345
1346
|
* ```ts
|
|
1346
|
-
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
1347
|
-
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
1348
1347
|
* import {
|
|
1349
1348
|
* asResourceKey,
|
|
1350
1349
|
* asRobloxAssetId,
|
|
1351
1350
|
* asSha256Hex,
|
|
1352
|
-
*
|
|
1351
|
+
* type ResourceDriver,
|
|
1353
1352
|
* } from "@bedrock-rbx/core";
|
|
1354
1353
|
*
|
|
1355
|
-
* const
|
|
1356
|
-
* async
|
|
1354
|
+
* const gamePassDriver: ResourceDriver<"gamePass"> = {
|
|
1355
|
+
* async create(desired) {
|
|
1357
1356
|
* return {
|
|
1358
|
-
* data: {
|
|
1357
|
+
* data: {
|
|
1358
|
+
* ...desired,
|
|
1359
|
+
* outputs: {
|
|
1360
|
+
* assetId: asRobloxAssetId("9876543210"),
|
|
1361
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
|
|
1362
|
+
* },
|
|
1363
|
+
* },
|
|
1359
1364
|
* success: true,
|
|
1360
1365
|
* };
|
|
1361
1366
|
* },
|
|
1362
1367
|
* };
|
|
1363
1368
|
*
|
|
1364
|
-
*
|
|
1365
|
-
* client: new PlacesClient({
|
|
1366
|
-
* apiKey: "rbx-your-key",
|
|
1367
|
-
* httpClient,
|
|
1368
|
-
* sleep: async () => {},
|
|
1369
|
-
* }),
|
|
1370
|
-
* readFile: async () =>
|
|
1371
|
-
* new Uint8Array([
|
|
1372
|
-
* 0x3c, 0x72, 0x6f, 0x62, 0x6c, 0x6f, 0x78, 0x21, 0x89, 0xff, 0x0d, 0x0a, 0x1a,
|
|
1373
|
-
* 0x0a,
|
|
1374
|
-
* ]),
|
|
1375
|
-
* universeId: asRobloxAssetId("1234567890"),
|
|
1376
|
-
* });
|
|
1377
|
-
*
|
|
1378
|
-
* return driver
|
|
1369
|
+
* return gamePassDriver
|
|
1379
1370
|
* .create({
|
|
1380
|
-
* description:
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
* "
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1371
|
+
* description: "Grants VIP perks.",
|
|
1372
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
1373
|
+
* iconFileHashes: {
|
|
1374
|
+
* "en-us": asSha256Hex(
|
|
1375
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1376
|
+
* ),
|
|
1377
|
+
* },
|
|
1378
|
+
* key: asResourceKey("vip-pass"),
|
|
1379
|
+
* kind: "gamePass",
|
|
1380
|
+
* name: "VIP Pass",
|
|
1381
|
+
* price: undefined,
|
|
1390
1382
|
* })
|
|
1391
1383
|
* .then((result) => {
|
|
1392
1384
|
* expect(result.success).toBeTrue();
|
|
1393
1385
|
* if (result.success) {
|
|
1394
|
-
* expect(result.data.outputs.
|
|
1386
|
+
* expect(result.data.outputs.assetId).toBe("9876543210");
|
|
1395
1387
|
* }
|
|
1396
1388
|
* });
|
|
1397
1389
|
* ```
|
|
1398
1390
|
*/
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
*
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1391
|
+
interface ResourceDriver<K extends ResourceKind> {
|
|
1392
|
+
/**
|
|
1393
|
+
* Create the resource upstream from its desired state and return the
|
|
1394
|
+
* resulting current state (desired fields + Roblox-assigned outputs).
|
|
1395
|
+
*/
|
|
1396
|
+
create(desired: Extract<ResourceDesiredState, {
|
|
1397
|
+
kind: K;
|
|
1398
|
+
}>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
|
|
1399
|
+
/**
|
|
1400
|
+
* Reconcile an upstream resource whose managed content has drifted from its
|
|
1401
|
+
* desired state. Receives the last-known current state so the driver can
|
|
1402
|
+
* compute a minimal patch (or no-op upstream, for file-backed kinds where
|
|
1403
|
+
* republishing is unconditional).
|
|
1404
|
+
*
|
|
1405
|
+
* Optional. Drivers whose upstream API has no update operation omit this
|
|
1406
|
+
* method; `applyOps` surfaces an `updateUnsupported` error at dispatch time
|
|
1407
|
+
* instead.
|
|
1408
|
+
*/
|
|
1409
|
+
update?(current: ResourceCurrentState<K>, desired: Extract<ResourceDesiredState, {
|
|
1410
|
+
kind: K;
|
|
1411
|
+
}>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
|
|
1415
1412
|
}
|
|
1416
1413
|
/**
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
* A `NotFound` error (HTTP 404) from `UniversesClient.update` is repackaged
|
|
1423
|
-
* as an adoption-error `ApiError` whose message names the config key and
|
|
1424
|
-
* the `universeId`, so operators can tell adoption failure apart from
|
|
1425
|
-
* transient upstream errors. A successful response whose `rootPlaceId` is
|
|
1426
|
-
* absent surfaces as an `ApiError` with status 200, mirroring the
|
|
1427
|
-
* malformed-response guard in `GamePassDriver`.
|
|
1428
|
-
*
|
|
1429
|
-
* When `displayName` is declared, the driver routes that field through
|
|
1430
|
-
* `PlacesClient.update` on the root place after the universe PATCH
|
|
1431
|
-
* succeeds. A subsequent places failure surfaces to the caller as the
|
|
1432
|
-
* driver's error result without rolling back the prior universe patch,
|
|
1433
|
-
* so callers observing a partial failure should reconcile by
|
|
1434
|
-
* reapplying rather than assuming the universe-level fields are
|
|
1435
|
-
* unchanged.
|
|
1436
|
-
*
|
|
1437
|
-
* @param deps - Injected ocale clients (universes plus places for the
|
|
1438
|
-
* read-only universe fields Roblox derives from the root place).
|
|
1439
|
-
* @returns A driver indexable by `"universe"` in a `DriverRegistry`.
|
|
1414
|
+
* Polymorphic dispatch table keyed by {@link ResourceKind}, mapping each kind
|
|
1415
|
+
* to the {@link ResourceDriver} that handles it. `applyOps` indexes the
|
|
1416
|
+
* registry by `op.desired.kind` to reach the matching driver with full type
|
|
1417
|
+
* safety: adding a new kind to `ResourceDesiredState` is a compile error until
|
|
1418
|
+
* a matching registry entry is supplied.
|
|
1440
1419
|
*
|
|
1441
1420
|
* @example
|
|
1442
1421
|
*
|
|
1443
1422
|
* ```ts
|
|
1444
|
-
* import type
|
|
1445
|
-
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
1446
|
-
* import { UniversesClient } from "@bedrock-rbx/ocale/universes";
|
|
1447
|
-
* import { validUniverseBody } from "@bedrock-rbx/ocale/testing";
|
|
1448
|
-
* import {
|
|
1449
|
-
* asRobloxAssetId,
|
|
1450
|
-
* createUniverseDriver,
|
|
1451
|
-
* UNIVERSE_SINGLETON_KEY,
|
|
1452
|
-
* } from "@bedrock-rbx/core";
|
|
1423
|
+
* import { OpenCloudError, type DriverRegistry } from "@bedrock-rbx/core";
|
|
1453
1424
|
*
|
|
1454
|
-
* const
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1425
|
+
* const registry: DriverRegistry = {
|
|
1426
|
+
* gamePass: {
|
|
1427
|
+
* async create() {
|
|
1428
|
+
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
1429
|
+
* },
|
|
1430
|
+
* },
|
|
1431
|
+
* place: {
|
|
1432
|
+
* async create() {
|
|
1433
|
+
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
1434
|
+
* },
|
|
1435
|
+
* },
|
|
1436
|
+
* universe: {
|
|
1437
|
+
* async create() {
|
|
1438
|
+
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
1439
|
+
* },
|
|
1440
|
+
* },
|
|
1441
|
+
* developerProduct: {
|
|
1442
|
+
* async create() {
|
|
1443
|
+
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
1444
|
+
* },
|
|
1467
1445
|
* },
|
|
1468
1446
|
* };
|
|
1469
1447
|
*
|
|
1470
|
-
*
|
|
1471
|
-
* places: new PlacesClient({
|
|
1472
|
-
* apiKey: "rbx-your-key",
|
|
1473
|
-
* httpClient: universeBodyHttpClient,
|
|
1474
|
-
* sleep: async () => {},
|
|
1475
|
-
* }),
|
|
1476
|
-
* universes: new UniversesClient({
|
|
1477
|
-
* apiKey: "rbx-your-key",
|
|
1478
|
-
* httpClient: universeBodyHttpClient,
|
|
1479
|
-
* sleep: async () => {},
|
|
1480
|
-
* }),
|
|
1481
|
-
* });
|
|
1482
|
-
*
|
|
1483
|
-
* return driver
|
|
1484
|
-
* .create({
|
|
1485
|
-
* consoleEnabled: undefined,
|
|
1486
|
-
* desktopEnabled: true,
|
|
1487
|
-
* displayName: undefined,
|
|
1488
|
-
* key: UNIVERSE_SINGLETON_KEY,
|
|
1489
|
-
* kind: "universe",
|
|
1490
|
-
* mobileEnabled: undefined,
|
|
1491
|
-
* privateServerPriceRobux: undefined,
|
|
1492
|
-
* tabletEnabled: undefined,
|
|
1493
|
-
* universeId: asRobloxAssetId("1234567890"),
|
|
1494
|
-
* voiceChatEnabled: true,
|
|
1495
|
-
* vrEnabled: undefined,
|
|
1496
|
-
* })
|
|
1497
|
-
* .then((result) => {
|
|
1498
|
-
* expect(result.success).toBeTrue();
|
|
1499
|
-
* if (result.success) {
|
|
1500
|
-
* expect(result.data.outputs.rootPlaceId).toBe("4711");
|
|
1501
|
-
* }
|
|
1502
|
-
* });
|
|
1503
|
-
* ```
|
|
1504
|
-
*/
|
|
1505
|
-
declare function createUniverseDriver(deps: UniverseDriverDeps): ResourceDriver<"universe">;
|
|
1506
|
-
//#endregion
|
|
1507
|
-
//#region src/core/derive-price-fields.d.ts
|
|
1508
|
-
/**
|
|
1509
|
-
* Wire-shape pricing fragment produced by {@link derivePriceFields}: the
|
|
1510
|
-
* `isForSale` flag and an optional numeric `price`. Mirrors the multipart
|
|
1511
|
-
* fields the Open Cloud `developer-products` create and update endpoints
|
|
1512
|
-
* accept for setting Robux pricing.
|
|
1513
|
-
*/
|
|
1514
|
-
interface PriceFields {
|
|
1515
|
-
/** Whether the developer product should be purchasable. */
|
|
1516
|
-
readonly isForSale: boolean;
|
|
1517
|
-
/** Default price in Robux; absent when the product is off-sale. */
|
|
1518
|
-
readonly price?: number;
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Translate a Mantle-style optional price into the Open Cloud wire shape.
|
|
1522
|
-
*
|
|
1523
|
-
* `desired.price === undefined` (no price declared) becomes
|
|
1524
|
-
* `{ isForSale: false }` and the `price` key is omitted entirely. A defined
|
|
1525
|
-
* price (including `0`) becomes `{ isForSale: true, price }`. Both
|
|
1526
|
-
* `developerProduct` create and update paths share this helper so the
|
|
1527
|
-
* "absent ⇒ off-sale" semantics live in exactly one place.
|
|
1528
|
-
*
|
|
1529
|
-
* @param desired - Object carrying the user-declared `price`.
|
|
1530
|
-
* @returns The wire-shape `{ isForSale, price? }` fragment.
|
|
1531
|
-
*
|
|
1532
|
-
* @example
|
|
1533
|
-
*
|
|
1534
|
-
* ```ts
|
|
1535
|
-
* import { derivePriceFields } from "@bedrock-rbx/core";
|
|
1536
|
-
*
|
|
1537
|
-
* expect(derivePriceFields({ price: undefined })).toStrictEqual({ isForSale: false });
|
|
1538
|
-
* expect(derivePriceFields({ price: 250 })).toStrictEqual({ isForSale: true, price: 250 });
|
|
1448
|
+
* expect(registry.gamePass).toBeObject();
|
|
1539
1449
|
* ```
|
|
1540
1450
|
*/
|
|
1541
|
-
|
|
1542
|
-
readonly price: number | undefined;
|
|
1543
|
-
}): PriceFields;
|
|
1451
|
+
type DriverRegistry = { [K in ResourceKind]: ResourceDriver<K> };
|
|
1544
1452
|
//#endregion
|
|
1545
1453
|
//#region src/core/operations.d.ts
|
|
1546
1454
|
/**
|
|
@@ -1738,162 +1646,228 @@ interface NoopOperation extends BaseOperation {
|
|
|
1738
1646
|
*/
|
|
1739
1647
|
type Operation = CreateOperation | NoopOperation | UpdateOperation;
|
|
1740
1648
|
//#endregion
|
|
1741
|
-
//#region src/
|
|
1649
|
+
//#region src/shell/apply-ops.d.ts
|
|
1742
1650
|
/**
|
|
1743
|
-
*
|
|
1744
|
-
*
|
|
1745
|
-
*
|
|
1746
|
-
* Each entry in `desired` is matched to `current` by `(kind, key)`: resources
|
|
1747
|
-
* are uniquely identified by that pair, so a `place` and a `universe` keyed
|
|
1748
|
-
* `"main"` are independent slots. A `(kind, key)` pair present only in
|
|
1749
|
-
* `desired` produces a `create` op; a pair present in both produces an
|
|
1750
|
-
* `update` op if any declared field differs or a `noop` op if every field
|
|
1751
|
-
* matches.
|
|
1752
|
-
*
|
|
1753
|
-
* Ops appear in the order their desired entries appear in the input array so
|
|
1754
|
-
* callers can rely on declaration order when logging or applying ops.
|
|
1651
|
+
* Failure surfaced by `applyOps` when an operation cannot be applied.
|
|
1652
|
+
* Plain-data discriminated union; narrow on `kind`, do not `instanceof` it.
|
|
1755
1653
|
*
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
1758
|
-
*
|
|
1759
|
-
*
|
|
1654
|
+
* `appliedSoFar` carries the driver outputs from operations that succeeded
|
|
1655
|
+
* before the failing one, in dispatched order. Callers persist this so a
|
|
1656
|
+
* follow-up reconcile does not duplicate Roblox-side resources that have
|
|
1657
|
+
* already been created or updated.
|
|
1760
1658
|
*
|
|
1761
1659
|
* @example
|
|
1762
1660
|
*
|
|
1763
1661
|
* ```ts
|
|
1764
|
-
* import {
|
|
1765
|
-
* asResourceKey,
|
|
1766
|
-
* asRobloxAssetId,
|
|
1767
|
-
* asSha256Hex,
|
|
1768
|
-
* diff,
|
|
1769
|
-
* type GamePassDesiredState,
|
|
1770
|
-
* type ResourceCurrentState,
|
|
1771
|
-
* } from "@bedrock-rbx/core";
|
|
1662
|
+
* import { asResourceKey, type ApplyError } from "@bedrock-rbx/core";
|
|
1772
1663
|
*
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1664
|
+
* function describe(err: ApplyError): string {
|
|
1665
|
+
* switch (err.kind) {
|
|
1666
|
+
* case "driverFailure": {
|
|
1667
|
+
* return `driver failed for ${err.key}: ${err.cause.message}`;
|
|
1668
|
+
* }
|
|
1669
|
+
* case "updateUnsupported": {
|
|
1670
|
+
* return `update not supported for ${err.key}`;
|
|
1671
|
+
* }
|
|
1672
|
+
* }
|
|
1673
|
+
* }
|
|
1776
1674
|
*
|
|
1777
|
-
* const
|
|
1778
|
-
* description: "Grants VIP perks.",
|
|
1779
|
-
* icon: { "en-us": "assets/vip-icon.png" },
|
|
1780
|
-
* iconFileHashes: { "en-us": hash },
|
|
1675
|
+
* const err: ApplyError = {
|
|
1781
1676
|
* key: asResourceKey("vip-pass"),
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
* price: 500,
|
|
1785
|
-
* };
|
|
1786
|
-
* const drifted: GamePassDesiredState = {
|
|
1787
|
-
* ...unchanged,
|
|
1788
|
-
* key: asResourceKey("legend-pass"),
|
|
1789
|
-
* name: "Legend Pass (renamed)",
|
|
1790
|
-
* };
|
|
1791
|
-
* const fresh: GamePassDesiredState = {
|
|
1792
|
-
* ...unchanged,
|
|
1793
|
-
* key: asResourceKey("rookie-pass"),
|
|
1794
|
-
* name: "Rookie Pass",
|
|
1677
|
+
* appliedSoFar: [],
|
|
1678
|
+
* kind: "updateUnsupported",
|
|
1795
1679
|
* };
|
|
1796
1680
|
*
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1681
|
+
* expect(describe(err)).toBe("update not supported for vip-pass");
|
|
1682
|
+
* ```
|
|
1683
|
+
*/
|
|
1684
|
+
type ApplyError = {
|
|
1685
|
+
readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
|
|
1686
|
+
readonly cause: OpenCloudError$1;
|
|
1687
|
+
readonly key: ResourceKey;
|
|
1688
|
+
readonly kind: "driverFailure";
|
|
1689
|
+
} | {
|
|
1690
|
+
readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
|
|
1691
|
+
readonly key: ResourceKey;
|
|
1692
|
+
readonly kind: "updateUnsupported";
|
|
1693
|
+
};
|
|
1694
|
+
/**
|
|
1695
|
+
* Dispatch each reconciliation operation to the matching resource driver
|
|
1696
|
+
* with first-fail semantics: on the first `Err` (driver failure or
|
|
1697
|
+
* `updateUnsupported`), the remaining operations are skipped and the error
|
|
1698
|
+
* is returned verbatim.
|
|
1699
|
+
*
|
|
1700
|
+
* Behaviour:
|
|
1701
|
+
* - `create` operations are routed to `registry[op.desired.kind].create`.
|
|
1702
|
+
* - `update` operations are routed to `registry[op.desired.kind].update`
|
|
1703
|
+
* when the driver exposes it; otherwise they short-circuit to an
|
|
1704
|
+
* `updateUnsupported` Err without invoking the driver.
|
|
1705
|
+
* - `noop` operations are skipped entirely (no I/O, no dispatch).
|
|
1706
|
+
*
|
|
1707
|
+
* On success the returned array carries the driver outputs for every
|
|
1708
|
+
* non-noop op, in dispatched order. Noops are not represented; callers
|
|
1709
|
+
* needing a full post-apply snapshot merge with the pre-apply current
|
|
1710
|
+
* state keyed by `ResourceKey`.
|
|
1711
|
+
*
|
|
1712
|
+
* @param ops - Reconciliation operations produced by `diff`, applied in order.
|
|
1713
|
+
* @param registry - Per-kind driver table; dispatch uses `op.desired.kind` as the index.
|
|
1714
|
+
* @returns `Ok(state)` when every operation succeeds, where `state` holds
|
|
1715
|
+
* driver outputs for each non-noop op in dispatched order; or the first
|
|
1716
|
+
* failure encountered.
|
|
1717
|
+
* @throws Whatever the dispatched driver rejects with outside its `Result`
|
|
1718
|
+
* return. A driver whose injected I/O (file reads, network calls, etc.)
|
|
1719
|
+
* throws will surface that rejection here rather than translating it into
|
|
1720
|
+
* a `Result` failure; wrap the call site in a try/catch when drivers are
|
|
1721
|
+
* not trusted to contain their own rejections.
|
|
1722
|
+
* @example
|
|
1723
|
+
*
|
|
1724
|
+
* ```ts
|
|
1725
|
+
* import {
|
|
1726
|
+
* applyOps,
|
|
1727
|
+
* asResourceKey,
|
|
1728
|
+
* asRobloxAssetId,
|
|
1729
|
+
* asSha256Hex,
|
|
1730
|
+
* type DriverRegistry,
|
|
1731
|
+
* type Operation,
|
|
1732
|
+
* } from "@bedrock-rbx/core";
|
|
1733
|
+
*
|
|
1734
|
+
* const registry: DriverRegistry = {
|
|
1735
|
+
* gamePass: {
|
|
1736
|
+
* async create(desired) {
|
|
1737
|
+
* return {
|
|
1738
|
+
* data: {
|
|
1739
|
+
* ...desired,
|
|
1740
|
+
* outputs: {
|
|
1741
|
+
* assetId: asRobloxAssetId("9876543210"),
|
|
1742
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
|
|
1743
|
+
* },
|
|
1744
|
+
* },
|
|
1745
|
+
* success: true,
|
|
1746
|
+
* };
|
|
1747
|
+
* },
|
|
1748
|
+
* },
|
|
1749
|
+
* place: {
|
|
1750
|
+
* async create(desired) {
|
|
1751
|
+
* return {
|
|
1752
|
+
* data: { ...desired, outputs: { versionNumber: 1 } },
|
|
1753
|
+
* success: true,
|
|
1754
|
+
* };
|
|
1755
|
+
* },
|
|
1756
|
+
* },
|
|
1757
|
+
* universe: {
|
|
1758
|
+
* async create(desired) {
|
|
1759
|
+
* return {
|
|
1760
|
+
* data: { ...desired, outputs: { rootPlaceId: asRobloxAssetId("4711") } },
|
|
1761
|
+
* success: true,
|
|
1762
|
+
* };
|
|
1763
|
+
* },
|
|
1764
|
+
* },
|
|
1765
|
+
* developerProduct: {
|
|
1766
|
+
* async create(desired) {
|
|
1767
|
+
* return {
|
|
1768
|
+
* data: {
|
|
1769
|
+
* ...desired,
|
|
1770
|
+
* outputs: { productId: asRobloxAssetId("8172635495") },
|
|
1771
|
+
* },
|
|
1772
|
+
* success: true,
|
|
1773
|
+
* };
|
|
1803
1774
|
* },
|
|
1804
1775
|
* },
|
|
1776
|
+
* };
|
|
1777
|
+
*
|
|
1778
|
+
* const ops: ReadonlyArray<Operation> = [
|
|
1805
1779
|
* {
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1780
|
+
* key: asResourceKey("vip-pass"),
|
|
1781
|
+
* type: "create",
|
|
1782
|
+
* desired: {
|
|
1783
|
+
* key: asResourceKey("vip-pass"),
|
|
1784
|
+
* name: "VIP Pass",
|
|
1785
|
+
* description: "Grants VIP perks.",
|
|
1786
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
1787
|
+
* iconFileHashes: {
|
|
1788
|
+
* "en-us": asSha256Hex(
|
|
1789
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1790
|
+
* ),
|
|
1791
|
+
* },
|
|
1792
|
+
* kind: "gamePass",
|
|
1793
|
+
* price: 500,
|
|
1811
1794
|
* },
|
|
1812
1795
|
* },
|
|
1813
1796
|
* ];
|
|
1814
1797
|
*
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1817
|
-
*
|
|
1798
|
+
* return applyOps(ops, registry).then((result) => {
|
|
1799
|
+
* expect(result.success).toBe(true);
|
|
1800
|
+
* expect(result.success && result.data).toHaveLength(1);
|
|
1801
|
+
* });
|
|
1818
1802
|
* ```
|
|
1819
1803
|
*/
|
|
1820
|
-
declare function
|
|
1804
|
+
declare function applyOps(ops: ReadonlyArray<Operation>, registry: DriverRegistry): Promise<Result$1<ReadonlyArray<ResourceCurrentState>, ApplyError>>;
|
|
1821
1805
|
//#endregion
|
|
1822
|
-
//#region src/
|
|
1806
|
+
//#region src/shell/build-default-registry.d.ts
|
|
1823
1807
|
/**
|
|
1824
|
-
*
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1808
|
+
* Failure surfaced when default-constructing a registry needs a config
|
|
1809
|
+
* field that is not present. The deploy boundary wraps this in a
|
|
1810
|
+
* `DeployError` so the caller sees a typed Result instead of a downstream
|
|
1811
|
+
* driver error.
|
|
1827
1812
|
*/
|
|
1828
|
-
|
|
1813
|
+
interface RegistryConfigError {
|
|
1814
|
+
/** Suggested fix routed back to the caller. */
|
|
1815
|
+
readonly hint: string;
|
|
1816
|
+
/** Literal discriminator for narrowing. */
|
|
1817
|
+
readonly kind: "registryConfigMissing";
|
|
1818
|
+
/** Which config field was missing. */
|
|
1819
|
+
readonly missing: "universeId";
|
|
1820
|
+
}
|
|
1821
|
+
/** Inputs for {@link buildDefaultRegistry}. */
|
|
1822
|
+
interface BuildDefaultRegistryDeps {
|
|
1823
|
+
/** Resolved project config; supplies `universe.universeId` and is read for nothing else. */
|
|
1824
|
+
readonly config: ResolvedConfig;
|
|
1825
|
+
/** Reads an environment variable; injected so tests stay free of `process.env`. */
|
|
1826
|
+
readonly getEnv: (name: string) => string | undefined;
|
|
1827
|
+
/** Reader plumbed into kind-specific drivers that ingest file bytes. */
|
|
1828
|
+
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
1829
|
+
}
|
|
1829
1830
|
/**
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
1834
|
-
* - `{label}`: label as written.
|
|
1835
|
-
* - `{LABEL}`: upper-cased label.
|
|
1836
|
-
* - `{Label}`: capitalized label (first character upper, rest as written).
|
|
1837
|
-
*
|
|
1838
|
-
* Other characters in the template flow through verbatim.
|
|
1839
|
-
*
|
|
1840
|
-
* @param label - Environment label declared on `EnvironmentEntry.label`.
|
|
1841
|
-
* @param format - Template string. Falls back to
|
|
1842
|
-
* {@link DEFAULT_PREFIX_FORMAT} when omitted.
|
|
1843
|
-
* @returns The rendered prefix string.
|
|
1831
|
+
* Construct the default `DriverRegistry` from `config.universe.universeId`
|
|
1832
|
+
* and `BEDROCK_API_KEY`. Reads the API key via the injected `getEnv` seam
|
|
1833
|
+
* and surfaces `missingCredential` or `registryConfigMissing` as typed
|
|
1834
|
+
* Results instead of throwing.
|
|
1844
1835
|
*
|
|
1845
1836
|
* @example
|
|
1846
1837
|
*
|
|
1847
1838
|
* ```ts
|
|
1848
|
-
* import {
|
|
1839
|
+
* import { buildDefaultRegistry } from "@bedrock-rbx/core";
|
|
1849
1840
|
*
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1841
|
+
* const registry = buildDefaultRegistry({
|
|
1842
|
+
* config: {
|
|
1843
|
+
* environments: { production: {} },
|
|
1844
|
+
* state: { backend: "gist", gistId: "abc" },
|
|
1845
|
+
* universe: { universeId: "1234567890" },
|
|
1846
|
+
* },
|
|
1847
|
+
* getEnv: () => "rbx-test",
|
|
1848
|
+
* readFile: async () => new Uint8Array(),
|
|
1849
|
+
* });
|
|
1850
|
+
*
|
|
1851
|
+
* expect(registry.success).toBeTrue();
|
|
1853
1852
|
* ```
|
|
1853
|
+
*
|
|
1854
|
+
* @param deps - Validated config plus credential and file-reader seams.
|
|
1855
|
+
* @returns A `DriverRegistry` on success, or a typed Err describing the
|
|
1856
|
+
* missing API key or the missing universe declaration.
|
|
1854
1857
|
*/
|
|
1855
|
-
declare function
|
|
1858
|
+
declare function buildDefaultRegistry(deps: BuildDefaultRegistryDeps): Result$1<DriverRegistry, MissingCredentialError | RegistryConfigError>;
|
|
1856
1859
|
//#endregion
|
|
1857
|
-
//#region src/core/
|
|
1860
|
+
//#region src/core/flatten.d.ts
|
|
1858
1861
|
/**
|
|
1859
|
-
*
|
|
1860
|
-
*
|
|
1861
|
-
*
|
|
1862
|
-
*
|
|
1863
|
-
* their storage layout. This helper accepts letters, digits, `-`, and `_`
|
|
1864
|
-
* only, with length between 1 and 64, and returns a `StateError` for
|
|
1865
|
-
* anything outside that set so the adapter can fail loudly instead of
|
|
1866
|
-
* silently stripping characters.
|
|
1862
|
+
* Pre-I/O game-pass input the flattener emits. Extends the authored
|
|
1863
|
+
* `GamePassEntry` with the tag discriminator and the `ResourceKey`-branded
|
|
1864
|
+
* key so `buildDesired` can consume a flat tagged list and layer on the
|
|
1865
|
+
* SHA-256 icon digest.
|
|
1867
1866
|
*
|
|
1868
1867
|
* @example
|
|
1869
1868
|
*
|
|
1870
1869
|
* ```ts
|
|
1871
|
-
* import {
|
|
1872
|
-
*
|
|
1873
|
-
* const ok = validateEnvironmentName("production");
|
|
1874
|
-
* expect(ok.success).toBeTrue();
|
|
1875
|
-
*
|
|
1876
|
-
* const bad = validateEnvironmentName("prod/staging");
|
|
1877
|
-
* expect(bad.success).toBeFalse();
|
|
1878
|
-
* ```
|
|
1879
|
-
*
|
|
1880
|
-
* @param environment - Raw environment name supplied by a caller.
|
|
1881
|
-
* @returns `Ok(environment)` when the name is safe to use, or
|
|
1882
|
-
* `Err(StateError)` with a descriptive reason when it is not.
|
|
1883
|
-
*/
|
|
1884
|
-
declare function validateEnvironmentName(environment: string): Result$1<string, StateError>;
|
|
1885
|
-
//#endregion
|
|
1886
|
-
//#region src/core/flatten.d.ts
|
|
1887
|
-
/**
|
|
1888
|
-
* Pre-I/O game-pass input the flattener emits. Extends the authored
|
|
1889
|
-
* `GamePassEntry` with the tag discriminator and the `ResourceKey`-branded
|
|
1890
|
-
* key so `buildDesired` can consume a flat tagged list and layer on the
|
|
1891
|
-
* SHA-256 icon digest.
|
|
1892
|
-
*
|
|
1893
|
-
* @example
|
|
1894
|
-
*
|
|
1895
|
-
* ```ts
|
|
1896
|
-
* import { asResourceKey, type GamePassDesiredInput } from "@bedrock-rbx/core";
|
|
1870
|
+
* import { asResourceKey, type GamePassDesiredInput } from "@bedrock-rbx/core";
|
|
1897
1871
|
*
|
|
1898
1872
|
* const input: GamePassDesiredInput = {
|
|
1899
1873
|
* description: "Grants VIP perks.",
|
|
@@ -2110,47 +2084,6 @@ type ResourceDesiredInput = DeveloperProductDesiredInput | GamePassDesiredInput
|
|
|
2110
2084
|
*/
|
|
2111
2085
|
declare function flattenConfig(config: ResolvedConfig): ReadonlyArray<ResourceDesiredInput>;
|
|
2112
2086
|
//#endregion
|
|
2113
|
-
//#region src/core/get-environment.d.ts
|
|
2114
|
-
/**
|
|
2115
|
-
* Failure modes returned by {@link getEnvironment}.
|
|
2116
|
-
*/
|
|
2117
|
-
type GetEnvironmentError = {
|
|
2118
|
-
readonly kind: "missingEnvironment";
|
|
2119
|
-
} | {
|
|
2120
|
-
readonly kind: "multipleEnvironments";
|
|
2121
|
-
readonly values: ReadonlyArray<string>;
|
|
2122
|
-
};
|
|
2123
|
-
/**
|
|
2124
|
-
* Resolve the deploy environment for an override script invocation.
|
|
2125
|
-
*
|
|
2126
|
-
* Reads `--env <name>` from the supplied argv first, falls back to
|
|
2127
|
-
* `BEDROCK_ENVIRONMENT` from the supplied env reader. Returns
|
|
2128
|
-
* `missingEnvironment` when neither is present and `multipleEnvironments`
|
|
2129
|
-
* (with every offending value) when argv contains more than one `--env`
|
|
2130
|
-
* flag. Both inputs default to the running process so override scripts
|
|
2131
|
-
* under `.bedrock/` can call `getEnvironment()` with no arguments.
|
|
2132
|
-
*
|
|
2133
|
-
* @param argv - Argument list to scan for `--env <name>` flags. Defaults to
|
|
2134
|
-
* `process.argv.slice(2)` when omitted.
|
|
2135
|
-
* @param readEnvironment - Reads an environment variable; consulted as a
|
|
2136
|
-
* fallback when no `--env` flag is present. Defaults to a `process.env`
|
|
2137
|
-
* reader when omitted.
|
|
2138
|
-
* @returns `Ok(environment)` on success, `Err(GetEnvironmentError)` otherwise.
|
|
2139
|
-
* @example
|
|
2140
|
-
*
|
|
2141
|
-
* ```ts
|
|
2142
|
-
* import { getEnvironment } from "@bedrock-rbx/core";
|
|
2143
|
-
*
|
|
2144
|
-
* const result = getEnvironment(["--env", "production"], () => undefined);
|
|
2145
|
-
*
|
|
2146
|
-
* expect(result.success).toBeTrue();
|
|
2147
|
-
* if (result.success) {
|
|
2148
|
-
* expect(result.data).toBe("production");
|
|
2149
|
-
* }
|
|
2150
|
-
* ```
|
|
2151
|
-
*/
|
|
2152
|
-
declare function getEnvironment(argv?: ReadonlyArray<string>, readEnvironment?: (name: string) => string | undefined): Result$1<string, GetEnvironmentError>;
|
|
2153
|
-
//#endregion
|
|
2154
2087
|
//#region src/core/kinds/module.d.ts
|
|
2155
2088
|
/**
|
|
2156
2089
|
* Failure surfaced during desired-state preparation. Two variants today:
|
|
@@ -2325,1007 +2258,1284 @@ type InputFor<K extends ResourceKind> = Extract<ResourceDesiredInput, {
|
|
|
2325
2258
|
readonly kind: K;
|
|
2326
2259
|
}>;
|
|
2327
2260
|
//#endregion
|
|
2328
|
-
//#region src/
|
|
2261
|
+
//#region src/shell/build-desired.d.ts
|
|
2329
2262
|
/**
|
|
2330
|
-
*
|
|
2331
|
-
*
|
|
2332
|
-
* entry, signalling that the driver must re-upload before reconciling.
|
|
2333
|
-
* Returns `false` when the hashes match (no re-upload needed) and when
|
|
2334
|
-
* both sides report no icon.
|
|
2335
|
-
*
|
|
2336
|
-
* The signature takes hash maps directly (not whole-state) so the helper
|
|
2337
|
-
* is independent of any specific resource-kind shape; every icon-bearing
|
|
2338
|
-
* driver projects its own `iconFileHashes` and `outputs.iconFileHashes`
|
|
2339
|
-
* fields before calling.
|
|
2263
|
+
* Layer file I/O onto a flat tagged list of resource inputs to produce
|
|
2264
|
+
* `ResourceDesiredState`.
|
|
2340
2265
|
*
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
2266
|
+
* For each input, reads the file bytes via the injected `readFile`, computes
|
|
2267
|
+
* the SHA-256 hex digest, and assembles the branded desired-state record
|
|
2268
|
+
* that `diff` consumes. Entries are processed sequentially so the first
|
|
2269
|
+
* failure's attribution is deterministic.
|
|
2345
2270
|
*
|
|
2271
|
+
* @param inputs - Flat tagged resource inputs from `flattenConfig`.
|
|
2272
|
+
* @param readFile - Reads file bytes for a given path; rejection becomes a
|
|
2273
|
+
* `fileReadFailed` Err.
|
|
2274
|
+
* @returns `Ok` with the desired-state array (same length and order as
|
|
2275
|
+
* `inputs`), or `Err` with the first I/O failure.
|
|
2346
2276
|
* @example
|
|
2347
2277
|
*
|
|
2348
2278
|
* ```ts
|
|
2349
|
-
* import {
|
|
2279
|
+
* import { asResourceKey, buildDesired } from "@bedrock-rbx/core";
|
|
2350
2280
|
*
|
|
2351
|
-
*
|
|
2352
|
-
*
|
|
2353
|
-
*
|
|
2354
|
-
* const fresh = asSha256Hex(
|
|
2355
|
-
* "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
|
|
2356
|
-
* );
|
|
2281
|
+
* async function readFile(): Promise<Uint8Array> {
|
|
2282
|
+
* return new Uint8Array([1, 2, 3]);
|
|
2283
|
+
* }
|
|
2357
2284
|
*
|
|
2358
|
-
*
|
|
2359
|
-
*
|
|
2285
|
+
* return buildDesired(
|
|
2286
|
+
* [
|
|
2287
|
+
* {
|
|
2288
|
+
* description: "Grants VIP perks.",
|
|
2289
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
2290
|
+
* key: asResourceKey("vip-pass"),
|
|
2291
|
+
* kind: "gamePass",
|
|
2292
|
+
* name: "VIP Pass",
|
|
2293
|
+
* price: 500,
|
|
2294
|
+
* },
|
|
2295
|
+
* ],
|
|
2296
|
+
* readFile,
|
|
2297
|
+
* ).then((result) => {
|
|
2298
|
+
* expect(result.success).toBeTrue();
|
|
2299
|
+
* if (result.success) {
|
|
2300
|
+
* expect(result.data).toHaveLength(1);
|
|
2301
|
+
* expect(result.data[0]!.kind).toBe("gamePass");
|
|
2302
|
+
* }
|
|
2303
|
+
* });
|
|
2360
2304
|
* ```
|
|
2361
2305
|
*/
|
|
2362
|
-
declare function
|
|
2306
|
+
declare function buildDesired(inputs: ReadonlyArray<ResourceDesiredInput>, readFile: (path: string) => Promise<Uint8Array>): Promise<Result$1<ReadonlyArray<ResourceDesiredState>, BuildDesiredError>>;
|
|
2363
2307
|
//#endregion
|
|
2364
|
-
//#region src/
|
|
2308
|
+
//#region src/shell/load-config.d.ts
|
|
2365
2309
|
/**
|
|
2366
|
-
*
|
|
2367
|
-
*
|
|
2368
|
-
|
|
2369
|
-
|
|
2310
|
+
* Options for {@link loadConfig}. Matches a subset of c12's loader options;
|
|
2311
|
+
* additional fields land with the issues that introduce each flow.
|
|
2312
|
+
*/
|
|
2313
|
+
interface LoadConfigOptions {
|
|
2314
|
+
/**
|
|
2315
|
+
* Path to a specific config file to load, including its extension.
|
|
2316
|
+
* Resolved relative to `cwd` when not absolute. Loaded as-is with no
|
|
2317
|
+
* extension search; if the file does not exist at the given path,
|
|
2318
|
+
* `loadConfig` returns `fileNotFound`. When omitted, `loadConfig`
|
|
2319
|
+
* discovers `bedrock.config.{ts,js,...}` from `cwd`.
|
|
2320
|
+
*/
|
|
2321
|
+
readonly configFile?: string;
|
|
2322
|
+
/**
|
|
2323
|
+
* Directory to search from. Defaults to `process.cwd()` at call time, so
|
|
2324
|
+
* each invocation sees the current working directory.
|
|
2325
|
+
*/
|
|
2326
|
+
readonly cwd?: string;
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Discover, parse, and validate the project config.
|
|
2330
|
+
*
|
|
2331
|
+
* Looks for `bedrock.config.{ts,js,mjs,cjs,yaml,yml,json}`, `.bedrockrc*`,
|
|
2332
|
+
* and `package.json#bedrock` starting at `options.cwd` (or the current
|
|
2333
|
+
* working directory). Returns a fresh, mutable `Config` on every call so
|
|
2334
|
+
* long-running scripts see up-to-date values.
|
|
2335
|
+
*
|
|
2336
|
+
* When the exported default is a function (sync or async), `loadConfig`
|
|
2337
|
+
* invokes it with an empty `ConfigContext` and awaits the result before
|
|
2338
|
+
* validating.
|
|
2339
|
+
*
|
|
2340
|
+
* Errors return via `Result`:
|
|
2341
|
+
* - `fileNotFound` - no config file was discovered under the search path.
|
|
2342
|
+
* - `parseFailed` - a config file was found but could not be parsed (for
|
|
2343
|
+
* example, malformed YAML or JSON).
|
|
2344
|
+
* - `validationFailed` - a file was found and parsed, but its content did
|
|
2345
|
+
* not satisfy the runtime schema.
|
|
2346
|
+
* - `configFunctionFailed` - a function-form config threw or its returned
|
|
2347
|
+
* promise rejected while being invoked.
|
|
2370
2348
|
*
|
|
2349
|
+
* @param options - Loader options.
|
|
2350
|
+
* @returns `Ok` with the validated `Config`, or `Err` with a `ConfigError`.
|
|
2371
2351
|
* @example
|
|
2372
2352
|
*
|
|
2373
2353
|
* ```ts
|
|
2374
|
-
* import {
|
|
2354
|
+
* import { loadConfig } from "@bedrock-rbx/core";
|
|
2375
2355
|
*
|
|
2376
|
-
*
|
|
2377
|
-
*
|
|
2378
|
-
*
|
|
2379
|
-
*
|
|
2356
|
+
* return loadConfig({
|
|
2357
|
+
* configFile: "bedrock.staging.config.yaml",
|
|
2358
|
+
* cwd: "/path/that/does/not/have/a/config",
|
|
2359
|
+
* }).then((result) => {
|
|
2360
|
+
* expect(result.success).toBeFalse();
|
|
2361
|
+
* if (!result.success) {
|
|
2362
|
+
* expect(result.err.kind).toBe("fileNotFound");
|
|
2363
|
+
* }
|
|
2364
|
+
* });
|
|
2380
2365
|
* ```
|
|
2381
2366
|
*/
|
|
2382
|
-
declare
|
|
2367
|
+
declare function loadConfig(options?: LoadConfigOptions): Promise<Result$1<Config, ConfigError>>;
|
|
2383
2368
|
//#endregion
|
|
2384
|
-
//#region src/
|
|
2369
|
+
//#region src/shell/deploy.d.ts
|
|
2385
2370
|
/**
|
|
2386
|
-
*
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
2389
|
-
* `JSON.stringify` for downstream logging and parallel-iterates cleanly
|
|
2390
|
-
* with `Config.environments` (which is itself a `Record`).
|
|
2371
|
+
* Inputs for `deploy`. Every field except `environment` is optional;
|
|
2372
|
+
* omitted dependencies are default-constructed from the project config
|
|
2373
|
+
* and the environment variables `GITHUB_TOKEN` and `BEDROCK_API_KEY`.
|
|
2391
2374
|
*/
|
|
2392
|
-
|
|
2375
|
+
interface DeployOptions {
|
|
2376
|
+
/** Pre-loaded, optionally-mutated project config. Omit to call `loadConfig()` automatically. */
|
|
2377
|
+
readonly config?: Config;
|
|
2378
|
+
/** Environment name; threaded into `StatePort.read` and the persisted snapshot. */
|
|
2379
|
+
readonly environment: string;
|
|
2380
|
+
/** `fetch` override plumbed into the default-constructed gist adapter when `statePort` is omitted. */
|
|
2381
|
+
readonly fetch?: GistFetch;
|
|
2382
|
+
/** Reads an environment variable; defaults to `(name) => process.env[name]`. */
|
|
2383
|
+
readonly getEnv?: (name: string) => string | undefined;
|
|
2384
|
+
/** Loader invoked when `config` is omitted; defaults to `loadConfig` from this package. */
|
|
2385
|
+
readonly loadConfig?: (options?: LoadConfigOptions) => Promise<Result$1<Config, ConfigError>>;
|
|
2386
|
+
/** Reads file bytes for resources that have file-backed inputs. Defaults to `node:fs/promises.readFile`. */
|
|
2387
|
+
readonly readFile?: (path: string) => Promise<Uint8Array>;
|
|
2388
|
+
/** Per-kind driver table consulted for create / update dispatch. Default-constructed from `BEDROCK_API_KEY` when omitted. */
|
|
2389
|
+
readonly registry?: DriverRegistry;
|
|
2390
|
+
/** Backend used to read the prior snapshot and persist the new one. Default-constructed from `config.state` and `GITHUB_TOKEN` when omitted. */
|
|
2391
|
+
readonly statePort?: StatePort;
|
|
2392
|
+
}
|
|
2393
2393
|
/**
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
2396
|
-
*
|
|
2394
|
+
* Failure surfaced by `deploy`. Stage-tagged so callers can branch on
|
|
2395
|
+
* `kind` to distinguish reconciliation failures (`stateReadFailed`,
|
|
2396
|
+
* `applyFailed`, ...) from default-construction failures
|
|
2397
|
+
* (`configLoadFailed`, `stateNotConfigured`, `unknownEnvironment`,
|
|
2398
|
+
* `incompletePlaceEntry`, `incompleteUniverseEntry`, `missingCredential`,
|
|
2399
|
+
* `unsupportedBackend`, `registryConfigMissing`).
|
|
2397
2400
|
*/
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
readonly
|
|
2401
|
-
|
|
2402
|
-
readonly
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
readonly
|
|
2407
|
-
}
|
|
2401
|
+
type DeployError = IncompletePassEntryError | IncompletePlaceEntryError | IncompleteUniverseEntryError | MissingCredentialError | RegistryConfigError | StateNotConfiguredError | UnknownEnvironmentError | UnsupportedBackendError | {
|
|
2402
|
+
readonly cause: ApplyError;
|
|
2403
|
+
readonly kind: "applyFailed";
|
|
2404
|
+
} | {
|
|
2405
|
+
readonly cause: BuildDesiredError;
|
|
2406
|
+
readonly kind: "buildDesiredFailed";
|
|
2407
|
+
} | {
|
|
2408
|
+
readonly cause: ConfigError;
|
|
2409
|
+
readonly kind: "configLoadFailed";
|
|
2410
|
+
} | {
|
|
2411
|
+
readonly cause: StateError;
|
|
2412
|
+
readonly kind: "stateReadFailed";
|
|
2413
|
+
} | {
|
|
2414
|
+
readonly cause: StateError;
|
|
2415
|
+
readonly kind: "stateWriteFailed";
|
|
2416
|
+
readonly unsavedState: BedrockState;
|
|
2417
|
+
};
|
|
2408
2418
|
/**
|
|
2409
|
-
*
|
|
2410
|
-
*
|
|
2419
|
+
* Run a full reconcile end-to-end. Default-constructs missing deps from
|
|
2420
|
+
* the project config and the environment variables `GITHUB_TOKEN` and
|
|
2421
|
+
* `BEDROCK_API_KEY`; never reads `process.env` when `statePort`,
|
|
2422
|
+
* `registry`, and `config` are all supplied explicitly.
|
|
2411
2423
|
*
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
2414
|
-
*
|
|
2415
|
-
*
|
|
2416
|
-
* - `interpretive` - the migrator applied a documented mapping rule
|
|
2417
|
-
* (cross-field fold, list-to-flag rewrite, URL-domain dispatch). Each
|
|
2418
|
-
* rule names the bedrock-side path it produced and the rule it followed
|
|
2419
|
-
* so the user can audit.
|
|
2420
|
-
* - `ambiguous` - the field is mappable but unsafe to act on without
|
|
2421
|
-
* user input; the migrator carries the hint forward instead of guessing.
|
|
2424
|
+
* @param options - Target environment plus optional overrides.
|
|
2425
|
+
* @returns The persisted `BedrockState` on success, or a stage-tagged
|
|
2426
|
+
* `DeployError` on failure.
|
|
2427
|
+
* @example
|
|
2422
2428
|
*
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2425
|
-
* `production.experienceConfiguration_singleton.genre`).
|
|
2426
|
-
*/
|
|
2427
|
-
type MigrationWarning = {
|
|
2428
|
-
readonly bedrockPath: string;
|
|
2429
|
-
readonly kind: "interpretive";
|
|
2430
|
-
readonly mantlePath: string;
|
|
2431
|
-
readonly rule: string;
|
|
2432
|
-
} | {
|
|
2433
|
-
readonly hint: string;
|
|
2434
|
-
readonly kind: "ambiguous";
|
|
2435
|
-
readonly mantlePath: string;
|
|
2436
|
-
} | {
|
|
2437
|
-
readonly kind: "blocked";
|
|
2438
|
-
readonly mantlePath: string;
|
|
2439
|
-
readonly reason: string;
|
|
2440
|
-
} | {
|
|
2441
|
-
readonly kind: "deferred";
|
|
2442
|
-
readonly mantlePath: string;
|
|
2443
|
-
readonly reason: string;
|
|
2444
|
-
};
|
|
2445
|
-
/**
|
|
2446
|
-
* Failure surfaced by `migrateMantleState`. Plain-data discriminated
|
|
2447
|
-
* union; narrow on `kind` rather than using `instanceof`.
|
|
2429
|
+
* ```ts
|
|
2430
|
+
* import { deploy } from "@bedrock-rbx/core";
|
|
2448
2431
|
*
|
|
2449
|
-
*
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
2452
|
-
*
|
|
2453
|
-
*
|
|
2454
|
-
*
|
|
2455
|
-
*
|
|
2456
|
-
*
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
2459
|
-
*
|
|
2460
|
-
*
|
|
2461
|
-
*
|
|
2462
|
-
*
|
|
2463
|
-
*
|
|
2464
|
-
*
|
|
2465
|
-
*
|
|
2432
|
+
* return deploy({ environment: "production" }).then((result) => {
|
|
2433
|
+
* expect(result.success).toBeFalse();
|
|
2434
|
+
* if (!result.success) {
|
|
2435
|
+
* expect(["configLoadFailed", "stateNotConfigured"]).toContain(result.err.kind);
|
|
2436
|
+
* }
|
|
2437
|
+
* });
|
|
2438
|
+
* ```
|
|
2439
|
+
*
|
|
2440
|
+
* @example
|
|
2441
|
+
*
|
|
2442
|
+
* ```ts
|
|
2443
|
+
* import { deploy, type BedrockState, type DriverRegistry, type StatePort } from "@bedrock-rbx/core";
|
|
2444
|
+
*
|
|
2445
|
+
* const store = new Map<string, BedrockState>();
|
|
2446
|
+
* const statePort: StatePort = {
|
|
2447
|
+
* async read(environment) {
|
|
2448
|
+
* return { data: store.get(environment), success: true };
|
|
2449
|
+
* },
|
|
2450
|
+
* async write(state) {
|
|
2451
|
+
* store.set(state.environment, state);
|
|
2452
|
+
* return { data: undefined, success: true };
|
|
2453
|
+
* },
|
|
2454
|
+
* };
|
|
2455
|
+
* const registry: DriverRegistry = {
|
|
2456
|
+
* developerProduct: {
|
|
2457
|
+
* create: async () => { throw new Error("unreachable: empty config"); },
|
|
2458
|
+
* },
|
|
2459
|
+
* gamePass: { create: async () => { throw new Error("unreachable: empty config"); } },
|
|
2460
|
+
* place: { create: async () => { throw new Error("unreachable: empty config"); } },
|
|
2461
|
+
* universe: { create: async () => { throw new Error("unreachable: empty config"); } },
|
|
2462
|
+
* };
|
|
2463
|
+
*
|
|
2464
|
+
* return deploy({
|
|
2465
|
+
* config: {
|
|
2466
|
+
* environments: { production: {} },
|
|
2467
|
+
* state: { backend: "gist", gistId: "abc" },
|
|
2468
|
+
* passes: {},
|
|
2469
|
+
* },
|
|
2470
|
+
* environment: "production",
|
|
2471
|
+
* registry,
|
|
2472
|
+
* statePort,
|
|
2473
|
+
* }).then((result) => {
|
|
2474
|
+
* expect(result.success).toBeTrue();
|
|
2475
|
+
* if (result.success) {
|
|
2476
|
+
* expect(result.data.environment).toBe("production");
|
|
2477
|
+
* expect(result.data.resources).toBeEmpty();
|
|
2478
|
+
* }
|
|
2479
|
+
* });
|
|
2480
|
+
* ```
|
|
2466
2481
|
*/
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
readonly primary: string;
|
|
2471
|
-
} | {
|
|
2472
|
-
readonly available: ReadonlyArray<string>;
|
|
2473
|
-
readonly kind: "primaryEnvironmentRequired";
|
|
2474
|
-
} | {
|
|
2475
|
-
readonly cause: ConfigError;
|
|
2476
|
-
readonly kind: "internalError";
|
|
2477
|
-
readonly reason: string;
|
|
2478
|
-
} | {
|
|
2479
|
-
readonly found: string;
|
|
2480
|
-
readonly kind: "unsupportedMantleStateVersion";
|
|
2481
|
-
readonly supported: ReadonlyArray<string>;
|
|
2482
|
-
} | {
|
|
2483
|
-
readonly kind: "stateFileNotFound";
|
|
2484
|
-
readonly path: string;
|
|
2485
|
-
} | {
|
|
2486
|
-
readonly kind: "stateParseFailed";
|
|
2487
|
-
readonly path: string;
|
|
2488
|
-
readonly reason: string;
|
|
2489
|
-
};
|
|
2482
|
+
declare function deploy(options: DeployOptions): Promise<Result$1<BedrockState, DeployError>>;
|
|
2483
|
+
//#endregion
|
|
2484
|
+
//#region src/cli/render.d.ts
|
|
2490
2485
|
/**
|
|
2491
|
-
*
|
|
2486
|
+
* Output port the CLI renders through. Mirrors the subset of `@clack/prompts`
|
|
2487
|
+
* the bedrock CLI uses today; tests inject a fake to assert what was rendered.
|
|
2492
2488
|
*
|
|
2493
|
-
*
|
|
2494
|
-
* already validated against the runtime schema (a failure to validate
|
|
2495
|
-
* surfaces as `MigrateError.internalError`, not as a returned report).
|
|
2489
|
+
* @example
|
|
2496
2490
|
*
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
*
|
|
2491
|
+
* ```ts
|
|
2492
|
+
* import type { ClackPort } from "@bedrock-rbx/core";
|
|
2493
|
+
*
|
|
2494
|
+
* const lines: Array<string> = [];
|
|
2495
|
+
* const port: ClackPort = {
|
|
2496
|
+
* cancel: (message) => lines.push(`cancel: ${message}`),
|
|
2497
|
+
* intro: (message) => lines.push(`intro: ${message}`),
|
|
2498
|
+
* logError: (message) => lines.push(`error: ${message}`),
|
|
2499
|
+
* logMessage: (message) => lines.push(`log: ${message}`),
|
|
2500
|
+
* logSuccess: (message) => lines.push(`ok: ${message}`),
|
|
2501
|
+
* outro: (message) => lines.push(`outro: ${message}`),
|
|
2502
|
+
* };
|
|
2500
2503
|
*
|
|
2501
|
-
*
|
|
2502
|
-
* environment from the input. Truthful per environment (no factorization)
|
|
2503
|
-
* so `bedrock deploy --env=<env>` produces zero ops on first run.
|
|
2504
|
+
* port.logSuccess("done");
|
|
2504
2505
|
*
|
|
2505
|
-
*
|
|
2506
|
-
*
|
|
2506
|
+
* expect(lines).toEqual(["ok: done"]);
|
|
2507
|
+
* ```
|
|
2507
2508
|
*/
|
|
2508
|
-
interface
|
|
2509
|
-
/**
|
|
2510
|
-
|
|
2511
|
-
/**
|
|
2512
|
-
|
|
2513
|
-
/**
|
|
2514
|
-
|
|
2515
|
-
/**
|
|
2516
|
-
|
|
2517
|
-
/**
|
|
2518
|
-
|
|
2509
|
+
interface ClackPort {
|
|
2510
|
+
/** End an interactive flow with a cancellation marker. */
|
|
2511
|
+
cancel(message: string): void;
|
|
2512
|
+
/** Open a framed section with a title (used for command intros). */
|
|
2513
|
+
intro(message: string): void;
|
|
2514
|
+
/** Render a single error line inside an open frame. */
|
|
2515
|
+
logError(message: string): void;
|
|
2516
|
+
/** Render a single neutral line inside an open frame. */
|
|
2517
|
+
logMessage(message: string): void;
|
|
2518
|
+
/** Render a single success line inside an open frame. */
|
|
2519
|
+
logSuccess(message: string): void;
|
|
2520
|
+
/** Close the current framed section with a final message. */
|
|
2521
|
+
outro(message: string): void;
|
|
2519
2522
|
}
|
|
2520
2523
|
//#endregion
|
|
2521
|
-
//#region src/
|
|
2524
|
+
//#region src/ports/progress-port.d.ts
|
|
2522
2525
|
/**
|
|
2523
|
-
*
|
|
2524
|
-
*
|
|
2525
|
-
*
|
|
2526
|
-
* where state should live.
|
|
2526
|
+
* Per-environment outcome event emitted after a deploy completes
|
|
2527
|
+
* successfully. Carries the environment name and the count of resources
|
|
2528
|
+
* present in the persisted state snapshot.
|
|
2527
2529
|
*/
|
|
2528
|
-
interface
|
|
2529
|
-
/**
|
|
2530
|
+
interface DeploySuccessEvent {
|
|
2531
|
+
/** The environment that finished reconciling. */
|
|
2530
2532
|
readonly environment: string;
|
|
2531
|
-
/**
|
|
2532
|
-
readonly kind: "
|
|
2533
|
+
/** Discriminator tag. */
|
|
2534
|
+
readonly kind: "deploySuccess";
|
|
2535
|
+
/** Number of resources in the post-deploy state snapshot. */
|
|
2536
|
+
readonly resourceCount: number;
|
|
2533
2537
|
}
|
|
2534
2538
|
/**
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
* the resolver to the discriminated-union arms.
|
|
2539
|
+
* Per-environment outcome event emitted when a deploy fails. Carries the
|
|
2540
|
+
* environment name and the full {@link DeployError} so a renderer can
|
|
2541
|
+
* delegate to the existing diagnostic helpers.
|
|
2539
2542
|
*/
|
|
2540
|
-
interface
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
readonly
|
|
2543
|
+
interface DeployFailureEvent {
|
|
2544
|
+
/** The environment whose deploy failed. */
|
|
2545
|
+
readonly environment: string;
|
|
2546
|
+
/** Stage-tagged failure reason returned by the shell `deploy` function. */
|
|
2547
|
+
readonly error: DeployError;
|
|
2548
|
+
/** Discriminator tag. */
|
|
2549
|
+
readonly kind: "deployFailure";
|
|
2545
2550
|
}
|
|
2546
2551
|
/**
|
|
2547
|
-
*
|
|
2548
|
-
*
|
|
2549
|
-
*
|
|
2550
|
-
|
|
2552
|
+
* Discriminated union of progress events the CLI emits while a deploy
|
|
2553
|
+
* runs. The variant set is additive: future per-stage and per-resource
|
|
2554
|
+
* events land as new `kind` values without breaking existing adapters.
|
|
2555
|
+
*/
|
|
2556
|
+
type ProgressEvent = DeployFailureEvent | DeploySuccessEvent;
|
|
2557
|
+
/**
|
|
2558
|
+
* Plugin contract for receiving deploy outcomes: the interface an adapter
|
|
2559
|
+
* (clack renderer, JSON logger, custom UI) implements to let the CLI hand
|
|
2560
|
+
* off events without re-implementing rendering logic.
|
|
2561
|
+
*
|
|
2562
|
+
* `ProgressPort` is a *driven* (secondary) port in hexagonal terms.
|
|
2551
2563
|
*
|
|
2552
|
-
* @param config - Validated project config.
|
|
2553
|
-
* @param environment - Target environment name.
|
|
2554
|
-
* @returns The resolved `StateConfig`, or `Err(stateNotConfigured)` when
|
|
2555
|
-
* neither the environment override nor the root block is set.
|
|
2556
2564
|
* @example
|
|
2557
2565
|
*
|
|
2558
2566
|
* ```ts
|
|
2559
|
-
* import {
|
|
2567
|
+
* import type { ProgressEvent, ProgressPort } from "@bedrock-rbx/core";
|
|
2560
2568
|
*
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
2565
|
-
* production: { state: { backend: "gist", gistId: "prod-gist" } },
|
|
2566
|
-
* },
|
|
2569
|
+
* let received: ReadonlyArray<ProgressEvent> = [];
|
|
2570
|
+
* const port: ProgressPort = {
|
|
2571
|
+
* emit(event) {
|
|
2572
|
+
* received = [...received, event];
|
|
2567
2573
|
* },
|
|
2568
|
-
*
|
|
2569
|
-
* );
|
|
2574
|
+
* };
|
|
2570
2575
|
*
|
|
2571
|
-
*
|
|
2572
|
-
*
|
|
2573
|
-
*
|
|
2574
|
-
* }
|
|
2576
|
+
* port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 });
|
|
2577
|
+
*
|
|
2578
|
+
* expect(received).toEqual([
|
|
2579
|
+
* { environment: "production", kind: "deploySuccess", resourceCount: 3 },
|
|
2580
|
+
* ]);
|
|
2575
2581
|
* ```
|
|
2576
2582
|
*/
|
|
2577
|
-
|
|
2583
|
+
interface ProgressPort {
|
|
2584
|
+
/** Hand a single progress event to the adapter for rendering or logging. */
|
|
2585
|
+
emit(event: ProgressEvent): void;
|
|
2586
|
+
}
|
|
2578
2587
|
//#endregion
|
|
2579
|
-
//#region src/
|
|
2588
|
+
//#region src/adapters/clack-progress-adapter.d.ts
|
|
2580
2589
|
/**
|
|
2581
|
-
*
|
|
2582
|
-
* name that is not a key of `config.environments`. Carries the list of
|
|
2583
|
-
* declared names so callers can render a "did you mean?" hint or a
|
|
2584
|
-
* close-match suggestion.
|
|
2590
|
+
* Configuration for {@link createClackProgressAdapter}.
|
|
2585
2591
|
*/
|
|
2586
|
-
interface
|
|
2587
|
-
/**
|
|
2588
|
-
readonly
|
|
2589
|
-
/** Environment name the caller asked for. */
|
|
2590
|
-
readonly environment: string;
|
|
2591
|
-
/** Literal discriminator for narrowing. */
|
|
2592
|
-
readonly kind: "unknownEnvironment";
|
|
2592
|
+
interface ClackProgressAdapterDeps {
|
|
2593
|
+
/** Output port the events are rendered through. */
|
|
2594
|
+
readonly clack: ClackPort;
|
|
2593
2595
|
}
|
|
2594
2596
|
/**
|
|
2595
|
-
*
|
|
2596
|
-
*
|
|
2597
|
-
*
|
|
2598
|
-
*
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2597
|
+
* Build a {@link ProgressPort} that renders events through a `ClackPort`.
|
|
2598
|
+
* Pattern-matches on the event `kind`: `deploySuccess` becomes a single
|
|
2599
|
+
* success line and `deployFailure` delegates to the package's deploy-error
|
|
2600
|
+
* rendering helper.
|
|
2601
|
+
*
|
|
2602
|
+
* @example
|
|
2603
|
+
*
|
|
2604
|
+
* ```ts
|
|
2605
|
+
* import { createClackProgressAdapter, type ClackPort } from "@bedrock-rbx/core";
|
|
2606
|
+
*
|
|
2607
|
+
* const lines: Array<string> = [];
|
|
2608
|
+
* const clack: ClackPort = {
|
|
2609
|
+
* cancel: (message) => lines.push(`cancel: ${message}`),
|
|
2610
|
+
* intro: (message) => lines.push(`intro: ${message}`),
|
|
2611
|
+
* logError: (message) => lines.push(`error: ${message}`),
|
|
2612
|
+
* logMessage: (message) => lines.push(`log: ${message}`),
|
|
2613
|
+
* logSuccess: (message) => lines.push(`ok: ${message}`),
|
|
2614
|
+
* outro: (message) => lines.push(`outro: ${message}`),
|
|
2615
|
+
* };
|
|
2616
|
+
*
|
|
2617
|
+
* const port = createClackProgressAdapter({ clack });
|
|
2618
|
+
*
|
|
2619
|
+
* port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 });
|
|
2620
|
+
*
|
|
2621
|
+
* expect(lines).toEqual(["ok: production: 3 resources reconciled"]);
|
|
2622
|
+
* ```
|
|
2623
|
+
*
|
|
2624
|
+
* @param deps - The clack port the adapter renders through.
|
|
2625
|
+
* @returns A `ProgressPort` that renders via clack.
|
|
2602
2626
|
*/
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
/** Environment whose overlay was projected onto the config. */
|
|
2607
|
-
readonly environment: string;
|
|
2608
|
-
/** Literal discriminator for narrowing. */
|
|
2609
|
-
readonly kind: "incompletePlaceEntry";
|
|
2610
|
-
/** Field that the merged entry lacks. */
|
|
2611
|
-
readonly missingField: "filePath" | "placeId";
|
|
2612
|
-
}
|
|
2627
|
+
declare function createClackProgressAdapter(deps: ClackProgressAdapterDeps): ProgressPort;
|
|
2628
|
+
//#endregion
|
|
2629
|
+
//#region src/adapters/developer-product-driver.d.ts
|
|
2613
2630
|
/**
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
*
|
|
2617
|
-
*
|
|
2618
|
-
*
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2631
|
+
* Dependencies of `createDeveloperProductDriver`. `universeId` is captured
|
|
2632
|
+
* at construction time (matching `GamePassDriverDeps`) so each driver
|
|
2633
|
+
* instance is bound to a single universe; multi-universe deploys construct
|
|
2634
|
+
* one driver per universe. `readFile` exists on the driver (not upstream
|
|
2635
|
+
* in shell) because icon hashes flow through `diff` but bytes do not.
|
|
2636
|
+
*
|
|
2637
|
+
* @example
|
|
2638
|
+
*
|
|
2639
|
+
* ```ts
|
|
2640
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2641
|
+
* import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
|
|
2642
|
+
* import { asRobloxAssetId, type DeveloperProductDriverDeps } from "@bedrock-rbx/core";
|
|
2643
|
+
*
|
|
2644
|
+
* const httpClient: HttpClient = {
|
|
2645
|
+
* async request() {
|
|
2646
|
+
* return { data: { body: {}, headers: {}, status: 200 }, success: true };
|
|
2647
|
+
* },
|
|
2648
|
+
* };
|
|
2649
|
+
*
|
|
2650
|
+
* const deps: DeveloperProductDriverDeps = {
|
|
2651
|
+
* client: new DeveloperProductsClient({
|
|
2652
|
+
* apiKey: "rbx-your-key",
|
|
2653
|
+
* httpClient,
|
|
2654
|
+
* sleep: async () => {},
|
|
2655
|
+
* }),
|
|
2656
|
+
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
2657
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2658
|
+
* };
|
|
2659
|
+
*
|
|
2660
|
+
* expect(deps.universeId).toBe("1234567890");
|
|
2661
|
+
* ```
|
|
2662
|
+
*/
|
|
2663
|
+
interface DeveloperProductDriverDeps {
|
|
2664
|
+
/** Configured developer-products client from `@bedrock-rbx/ocale/developer-products`. */
|
|
2665
|
+
readonly client: DeveloperProductsClient;
|
|
2666
|
+
/** Reads icon bytes for upload; rejections propagate out of `create` and `update`. */
|
|
2667
|
+
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
2668
|
+
/** Universe that owns every developer product this driver creates. */
|
|
2669
|
+
readonly universeId: RobloxAssetId;
|
|
2627
2670
|
}
|
|
2628
|
-
/** Failure modes returned by {@link selectEnvironment}. */
|
|
2629
|
-
type SelectEnvironmentError = IncompletePlaceEntryError | IncompleteUniverseEntryError | UnknownEnvironmentError;
|
|
2630
2671
|
/**
|
|
2631
|
-
*
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
2672
|
+
* Wraps {@link DeveloperProductsClient} as a `ResourceDriver<"developerProduct">`
|
|
2673
|
+
* that maps a desired-state entry to an ocale create or update call and the
|
|
2674
|
+
* response back to a `ResourceCurrentState<"developerProduct">`. The
|
|
2675
|
+
* `update` path consumes the upstream `204 No Content` response and
|
|
2676
|
+
* synthesizes the post-update `ResourceCurrentState` from `desired` plus
|
|
2677
|
+
* the existing `current.outputs`, carrying `iconImageAssetId` forward when
|
|
2678
|
+
* present.
|
|
2636
2679
|
*
|
|
2637
|
-
*
|
|
2638
|
-
* functions (`flattenConfig`, `buildDefaultRegistry`, `resolveStateConfig`).
|
|
2639
|
-
* The post-merge view promotes `places` from `Record<string, PlaceEntry>`
|
|
2640
|
-
* (root: file-paths only) to `Record<string, ResolvedPlaceEntry>` (root +
|
|
2641
|
-
* overlay merged). `environments` and `extends` are passed through
|
|
2642
|
-
* unchanged because they preserve the shape relationship to `Config`;
|
|
2643
|
-
* downstream consumers do not read them post-merge.
|
|
2680
|
+
* Upstream `OpenCloudError` results pass through as `Result` failures.
|
|
2644
2681
|
*
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2647
|
-
* entry whose overlay-supplied fields win), and `null` / `undefined` in
|
|
2648
|
-
* the overlay are skipped (so the overlay never deletes a root field).
|
|
2649
|
-
* State has its own resolution path (a single replacement, not a
|
|
2650
|
-
* deep-merge) because it is a tagged union: a deep-merge of
|
|
2651
|
-
* `{ backend: "s3" }` over `{ backend: "gist", gistId }` would produce
|
|
2652
|
-
* a malformed `{ backend: "s3", gistId }`.
|
|
2682
|
+
* @param deps - Injected ocale client and owning universe.
|
|
2683
|
+
* @returns A driver indexable by `"developerProduct"` in a `DriverRegistry`.
|
|
2653
2684
|
*
|
|
2654
|
-
*
|
|
2655
|
-
* provides one. Callers that require a resolved `StateConfig` should
|
|
2656
|
-
* route through `resolveStateConfig` or `buildStatePort`; the absent
|
|
2657
|
-
* case surfaces as a typed `stateNotConfigured` there.
|
|
2685
|
+
* @example
|
|
2658
2686
|
*
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
2661
|
-
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2665
|
-
*
|
|
2687
|
+
* ```ts
|
|
2688
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2689
|
+
* import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
|
|
2690
|
+
* import {
|
|
2691
|
+
* asResourceKey,
|
|
2692
|
+
* asRobloxAssetId,
|
|
2693
|
+
* createDeveloperProductDriver,
|
|
2694
|
+
* } from "@bedrock-rbx/core";
|
|
2695
|
+
*
|
|
2696
|
+
* const httpClient: HttpClient = {
|
|
2697
|
+
* async request() {
|
|
2698
|
+
* return {
|
|
2699
|
+
* data: {
|
|
2700
|
+
* body: {
|
|
2701
|
+
* createdTimestamp: "2024-01-15T10:30:00.000Z",
|
|
2702
|
+
* description: "Stocks the player up with 1,000 premium gems.",
|
|
2703
|
+
* iconImageAssetId: null,
|
|
2704
|
+
* isForSale: false,
|
|
2705
|
+
* isImmutable: false,
|
|
2706
|
+
* name: "Gem Pack",
|
|
2707
|
+
* priceInformation: null,
|
|
2708
|
+
* productId: 9_876_543_210,
|
|
2709
|
+
* storePageEnabled: false,
|
|
2710
|
+
* universeId: 1_234_567_890,
|
|
2711
|
+
* updatedTimestamp: "2024-01-15T10:30:00.000Z",
|
|
2712
|
+
* },
|
|
2713
|
+
* headers: {},
|
|
2714
|
+
* status: 200,
|
|
2715
|
+
* },
|
|
2716
|
+
* success: true,
|
|
2717
|
+
* };
|
|
2718
|
+
* },
|
|
2719
|
+
* };
|
|
2720
|
+
*
|
|
2721
|
+
* const driver = createDeveloperProductDriver({
|
|
2722
|
+
* client: new DeveloperProductsClient({
|
|
2723
|
+
* apiKey: "rbx-your-key",
|
|
2724
|
+
* httpClient,
|
|
2725
|
+
* sleep: async () => {},
|
|
2726
|
+
* }),
|
|
2727
|
+
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
2728
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2729
|
+
* });
|
|
2730
|
+
*
|
|
2731
|
+
* return driver
|
|
2732
|
+
* .create({
|
|
2733
|
+
* description: "Stocks the player up with 1,000 premium gems.",
|
|
2734
|
+
* isRegionalPricingEnabled: undefined,
|
|
2735
|
+
* key: asResourceKey("gem-pack"),
|
|
2736
|
+
* kind: "developerProduct",
|
|
2737
|
+
* name: "Gem Pack",
|
|
2738
|
+
* price: undefined,
|
|
2739
|
+
* storePageEnabled: undefined,
|
|
2740
|
+
* })
|
|
2741
|
+
* .then((result) => {
|
|
2742
|
+
* expect(result.success).toBeTrue();
|
|
2743
|
+
* if (result.success) {
|
|
2744
|
+
* expect(result.data.outputs.productId).toBe("9876543210");
|
|
2745
|
+
* }
|
|
2746
|
+
* });
|
|
2747
|
+
* ```
|
|
2748
|
+
*/
|
|
2749
|
+
declare function createDeveloperProductDriver(deps: DeveloperProductDriverDeps): ResourceDriver<"developerProduct">;
|
|
2750
|
+
//#endregion
|
|
2751
|
+
//#region src/adapters/game-pass-driver.d.ts
|
|
2752
|
+
/**
|
|
2753
|
+
* `universeId` is captured at construction time rather than on
|
|
2754
|
+
* `GamePassDesiredState` so state files round-trip with Mantle's `PassInputs`
|
|
2755
|
+
* shape. `readFile` exists on the driver (not upstream in shell) because icon
|
|
2756
|
+
* hashes flow through `diff` but bytes do not.
|
|
2757
|
+
*
|
|
2758
|
+
* @example
|
|
2759
|
+
*
|
|
2760
|
+
* ```ts
|
|
2761
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2762
|
+
* import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
|
|
2763
|
+
* import { asRobloxAssetId, type GamePassDriverDeps } from "@bedrock-rbx/core";
|
|
2764
|
+
*
|
|
2765
|
+
* const httpClient: HttpClient = {
|
|
2766
|
+
* async request() {
|
|
2767
|
+
* return { data: { body: {}, headers: {}, status: 200 }, success: true };
|
|
2768
|
+
* },
|
|
2769
|
+
* };
|
|
2770
|
+
*
|
|
2771
|
+
* const deps: GamePassDriverDeps = {
|
|
2772
|
+
* client: new GamePassesClient({
|
|
2773
|
+
* apiKey: "rbx-your-key",
|
|
2774
|
+
* httpClient,
|
|
2775
|
+
* sleep: async () => {},
|
|
2776
|
+
* }),
|
|
2777
|
+
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
2778
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2779
|
+
* };
|
|
2780
|
+
*
|
|
2781
|
+
* expect(deps.universeId).toBe("1234567890");
|
|
2782
|
+
* ```
|
|
2783
|
+
*/
|
|
2784
|
+
interface GamePassDriverDeps {
|
|
2785
|
+
/** Configured game-passes client from `@bedrock-rbx/ocale/game-passes`. */
|
|
2786
|
+
readonly client: GamePassesClient;
|
|
2787
|
+
/** Reads icon bytes for upload; rejections propagate out of `create`. */
|
|
2788
|
+
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
2789
|
+
/** Universe that owns every game pass this driver creates. */
|
|
2790
|
+
readonly universeId: RobloxAssetId;
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Wraps {@link GamePassesClient} as a `ResourceDriver<"gamePass">` that maps
|
|
2794
|
+
* a desired-state entry to an ocale create call and the response back to a
|
|
2795
|
+
* `ResourceCurrentState<"gamePass">`.
|
|
2796
|
+
*
|
|
2797
|
+
* Upstream `OpenCloudError` results pass through as `Result` failures.
|
|
2798
|
+
* Filesystem errors from `deps.readFile` do not fit the `OpenCloudError`
|
|
2799
|
+
* shape and propagate as promise rejections; shell callers are expected to
|
|
2800
|
+
* translate them if a unified error surface is required.
|
|
2801
|
+
*
|
|
2802
|
+
* @param deps - Injected ocale client, file reader, and owning universe.
|
|
2803
|
+
* @returns A driver indexable by `"gamePass"` in a `DriverRegistry`.
|
|
2804
|
+
* @throws Whatever `deps.readFile` rejects with.
|
|
2805
|
+
*
|
|
2806
|
+
* @example
|
|
2807
|
+
*
|
|
2808
|
+
* ```ts
|
|
2809
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2810
|
+
* import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
|
|
2811
|
+
* import {
|
|
2812
|
+
* asResourceKey,
|
|
2813
|
+
* asRobloxAssetId,
|
|
2814
|
+
* asSha256Hex,
|
|
2815
|
+
* createGamePassDriver,
|
|
2816
|
+
* } from "@bedrock-rbx/core";
|
|
2817
|
+
*
|
|
2818
|
+
* const httpClient: HttpClient = {
|
|
2819
|
+
* async request() {
|
|
2820
|
+
* return {
|
|
2821
|
+
* data: {
|
|
2822
|
+
* body: {
|
|
2823
|
+
* createdTimestamp: "2024-01-15T10:30:00.000Z",
|
|
2824
|
+
* description: "Grants VIP perks.",
|
|
2825
|
+
* gamePassId: 9_876_543_210,
|
|
2826
|
+
* iconAssetId: 1_122_334_455,
|
|
2827
|
+
* isForSale: true,
|
|
2828
|
+
* name: "VIP Pass",
|
|
2829
|
+
* updatedTimestamp: "2024-01-15T10:30:00.000Z",
|
|
2830
|
+
* },
|
|
2831
|
+
* headers: {},
|
|
2832
|
+
* status: 200,
|
|
2833
|
+
* },
|
|
2834
|
+
* success: true,
|
|
2835
|
+
* };
|
|
2836
|
+
* },
|
|
2837
|
+
* };
|
|
2838
|
+
*
|
|
2839
|
+
* const driver = createGamePassDriver({
|
|
2840
|
+
* client: new GamePassesClient({
|
|
2841
|
+
* apiKey: "rbx-your-key",
|
|
2842
|
+
* httpClient,
|
|
2843
|
+
* sleep: async () => {},
|
|
2844
|
+
* }),
|
|
2845
|
+
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
2846
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2847
|
+
* });
|
|
2848
|
+
*
|
|
2849
|
+
* return driver
|
|
2850
|
+
* .create({
|
|
2851
|
+
* description: "Grants VIP perks.",
|
|
2852
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
2853
|
+
* iconFileHashes: {
|
|
2854
|
+
* "en-us": asSha256Hex(
|
|
2855
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
2856
|
+
* ),
|
|
2857
|
+
* },
|
|
2858
|
+
* key: asResourceKey("vip-pass"),
|
|
2859
|
+
* kind: "gamePass",
|
|
2860
|
+
* name: "VIP Pass",
|
|
2861
|
+
* price: 500,
|
|
2862
|
+
* })
|
|
2863
|
+
* .then((result) => {
|
|
2864
|
+
* expect(result.success).toBeTrue();
|
|
2865
|
+
* if (result.success) {
|
|
2866
|
+
* expect(result.data.outputs.assetId).toBe("9876543210");
|
|
2867
|
+
* }
|
|
2868
|
+
* });
|
|
2869
|
+
* ```
|
|
2870
|
+
*/
|
|
2871
|
+
declare function createGamePassDriver(deps: GamePassDriverDeps): ResourceDriver<"gamePass">;
|
|
2872
|
+
//#endregion
|
|
2873
|
+
//#region src/adapters/no-op-progress-adapter.d.ts
|
|
2874
|
+
/**
|
|
2875
|
+
* Build a {@link ProgressPort} that silently drops every event. Useful for
|
|
2876
|
+
* tests and programmatic callers who want to invoke deploy logic without
|
|
2877
|
+
* any rendering.
|
|
2878
|
+
*
|
|
2879
|
+
* @example
|
|
2880
|
+
*
|
|
2881
|
+
* ```ts
|
|
2882
|
+
* import { createNoOpProgressAdapter } from "@bedrock-rbx/core";
|
|
2666
2883
|
*
|
|
2667
|
-
*
|
|
2668
|
-
*
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
*
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2884
|
+
* const port = createNoOpProgressAdapter();
|
|
2885
|
+
*
|
|
2886
|
+
* expect(() =>
|
|
2887
|
+
* port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 }),
|
|
2888
|
+
* ).not.toThrow();
|
|
2889
|
+
* ```
|
|
2890
|
+
*
|
|
2891
|
+
* @returns A `ProgressPort` whose `emit` method is a no-op.
|
|
2892
|
+
*/
|
|
2893
|
+
declare function createNoOpProgressAdapter(): ProgressPort;
|
|
2894
|
+
//#endregion
|
|
2895
|
+
//#region src/adapters/place-driver.d.ts
|
|
2896
|
+
/**
|
|
2897
|
+
* Dependencies of `createPlaceDriver`. `universeId` is captured at
|
|
2898
|
+
* construction time (matching `GamePassDriverDeps`) so each driver instance
|
|
2899
|
+
* is bound to a single universe; multi-universe deploys construct one driver
|
|
2900
|
+
* per universe. `readFile` is injected because `diff` operates on file hashes
|
|
2901
|
+
* while the driver is the only place that needs the raw bytes.
|
|
2674
2902
|
*
|
|
2675
2903
|
* @example
|
|
2676
2904
|
*
|
|
2677
2905
|
* ```ts
|
|
2678
|
-
* import {
|
|
2679
|
-
* import
|
|
2906
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2907
|
+
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
2908
|
+
* import { asRobloxAssetId, type PlaceDriverDeps } from "@bedrock-rbx/core";
|
|
2680
2909
|
*
|
|
2681
|
-
* const
|
|
2682
|
-
*
|
|
2683
|
-
*
|
|
2910
|
+
* const httpClient: HttpClient = {
|
|
2911
|
+
* async request() {
|
|
2912
|
+
* return { data: { body: {}, headers: {}, status: 200 }, success: true };
|
|
2684
2913
|
* },
|
|
2685
|
-
* state: { backend: "gist", gistId: "abc123" },
|
|
2686
|
-
* universe: { voiceChatEnabled: true },
|
|
2687
2914
|
* };
|
|
2688
2915
|
*
|
|
2689
|
-
* const
|
|
2916
|
+
* const deps: PlaceDriverDeps = {
|
|
2917
|
+
* client: new PlacesClient({
|
|
2918
|
+
* apiKey: "rbx-your-key",
|
|
2919
|
+
* httpClient,
|
|
2920
|
+
* sleep: async () => {},
|
|
2921
|
+
* }),
|
|
2922
|
+
* readFile: async () => new Uint8Array(),
|
|
2923
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2924
|
+
* };
|
|
2690
2925
|
*
|
|
2691
|
-
* expect(
|
|
2692
|
-
* if (result.success) {
|
|
2693
|
-
* expect(result.data.universe?.universeId).toBe("999");
|
|
2694
|
-
* expect(result.data.universe?.voiceChatEnabled).toBeTrue();
|
|
2695
|
-
* expect(result.data.state?.backend).toBe("gist");
|
|
2696
|
-
* }
|
|
2926
|
+
* expect(deps.universeId).toBe("1234567890");
|
|
2697
2927
|
* ```
|
|
2698
|
-
*
|
|
2699
|
-
* @param config - Validated project config carrying at least one
|
|
2700
|
-
* environment under `environments`.
|
|
2701
|
-
* @param environment - Environment name to project onto. Must be a key
|
|
2702
|
-
* of `config.environments`.
|
|
2703
|
-
* @returns `Ok(ResolvedConfig)` with the merged resource fields and the
|
|
2704
|
-
* resolved state, or `Err(SelectEnvironmentError)` describing why the
|
|
2705
|
-
* projection failed.
|
|
2706
2928
|
*/
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2929
|
+
interface PlaceDriverDeps {
|
|
2930
|
+
/** Configured places client from `@bedrock-rbx/ocale/places`. */
|
|
2931
|
+
readonly client: PlacesClient;
|
|
2932
|
+
/** Reads place-file bytes for upload; rejections propagate out of the driver. */
|
|
2933
|
+
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
2934
|
+
/** Universe that owns every place this driver publishes. */
|
|
2935
|
+
readonly universeId: RobloxAssetId;
|
|
2936
|
+
}
|
|
2710
2937
|
/**
|
|
2711
|
-
*
|
|
2712
|
-
*
|
|
2938
|
+
* Wraps {@link PlacesClient} as a `ResourceDriver<"place">`. `create` and
|
|
2939
|
+
* `update` are both thin wrappers over a shared publish helper because the
|
|
2940
|
+
* upstream Open Cloud call is identical either way: there is no "create
|
|
2941
|
+
* place" endpoint (the place is user-supplied input), only "publish version".
|
|
2713
2942
|
*
|
|
2714
|
-
*
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2717
|
-
*
|
|
2943
|
+
* Format is detected from the file extension (`.rbxl` → binary,
|
|
2944
|
+
* `.rbxlx` → XML); any other extension returns an `ApiError`-backed failure
|
|
2945
|
+
* without hitting the network.
|
|
2946
|
+
*
|
|
2947
|
+
* @param deps - Injected ocale client, file reader, and owning universe.
|
|
2948
|
+
* @returns A driver indexable by `"place"` in a `DriverRegistry`.
|
|
2949
|
+
* @throws Whatever `deps.readFile` rejects with.
|
|
2718
2950
|
*
|
|
2719
2951
|
* @example
|
|
2720
2952
|
*
|
|
2721
2953
|
* ```ts
|
|
2722
|
-
* import {
|
|
2954
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2955
|
+
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
2956
|
+
* import {
|
|
2957
|
+
* asResourceKey,
|
|
2958
|
+
* asRobloxAssetId,
|
|
2959
|
+
* asSha256Hex,
|
|
2960
|
+
* createPlaceDriver,
|
|
2961
|
+
* } from "@bedrock-rbx/core";
|
|
2723
2962
|
*
|
|
2724
|
-
* const
|
|
2725
|
-
*
|
|
2726
|
-
*
|
|
2727
|
-
*
|
|
2963
|
+
* const httpClient: HttpClient = {
|
|
2964
|
+
* async request() {
|
|
2965
|
+
* return {
|
|
2966
|
+
* data: { body: { versionNumber: 1 }, headers: {}, status: 200 },
|
|
2967
|
+
* success: true,
|
|
2968
|
+
* };
|
|
2969
|
+
* },
|
|
2728
2970
|
* };
|
|
2729
2971
|
*
|
|
2730
|
-
* const
|
|
2731
|
-
*
|
|
2732
|
-
*
|
|
2733
|
-
*
|
|
2734
|
-
*
|
|
2972
|
+
* const driver = createPlaceDriver({
|
|
2973
|
+
* client: new PlacesClient({
|
|
2974
|
+
* apiKey: "rbx-your-key",
|
|
2975
|
+
* httpClient,
|
|
2976
|
+
* sleep: async () => {},
|
|
2977
|
+
* }),
|
|
2978
|
+
* readFile: async () =>
|
|
2979
|
+
* new Uint8Array([
|
|
2980
|
+
* 0x3c, 0x72, 0x6f, 0x62, 0x6c, 0x6f, 0x78, 0x21, 0x89, 0xff, 0x0d, 0x0a, 0x1a,
|
|
2981
|
+
* 0x0a,
|
|
2982
|
+
* ]),
|
|
2983
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2735
2984
|
* });
|
|
2736
|
-
* ```
|
|
2737
2985
|
*
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2986
|
+
* return driver
|
|
2987
|
+
* .create({
|
|
2988
|
+
* description: undefined,
|
|
2989
|
+
* displayName: undefined,
|
|
2990
|
+
* fileHash: asSha256Hex(
|
|
2991
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
2992
|
+
* ),
|
|
2993
|
+
* filePath: "places/start.rbxl",
|
|
2994
|
+
* key: asResourceKey("start-place"),
|
|
2995
|
+
* kind: "place",
|
|
2996
|
+
* placeId: asRobloxAssetId("4711"),
|
|
2997
|
+
* serverSize: undefined,
|
|
2998
|
+
* })
|
|
2999
|
+
* .then((result) => {
|
|
3000
|
+
* expect(result.success).toBeTrue();
|
|
3001
|
+
* if (result.success) {
|
|
3002
|
+
* expect(result.data.outputs.versionNumber).toBe(1);
|
|
3003
|
+
* }
|
|
3004
|
+
* });
|
|
3005
|
+
* ```
|
|
2740
3006
|
*/
|
|
2741
|
-
declare function
|
|
3007
|
+
declare function createPlaceDriver(deps: PlaceDriverDeps): ResourceDriver<"place">;
|
|
3008
|
+
//#endregion
|
|
3009
|
+
//#region src/adapters/universe-driver.d.ts
|
|
2742
3010
|
/**
|
|
2743
|
-
*
|
|
3011
|
+
* Dependencies of `createUniverseDriver`. The driver reconciles the
|
|
3012
|
+
* universe singleton against both the universes endpoint and the root
|
|
3013
|
+
* place (for fields Roblox marks read-only on the universe, like
|
|
3014
|
+
* `displayName`). There is no `universeId` at construction time because
|
|
3015
|
+
* the universe *is* the resource the driver reconciles, so the ID rides
|
|
3016
|
+
* along on each `UniverseDesiredState`.
|
|
3017
|
+
*/
|
|
3018
|
+
interface UniverseDriverDeps {
|
|
3019
|
+
/** Configured places client from `@bedrock-rbx/ocale/places`. */
|
|
3020
|
+
readonly places: PlacesClient;
|
|
3021
|
+
/** Configured universes client from `@bedrock-rbx/ocale/universes`. */
|
|
3022
|
+
readonly universes: UniversesClient;
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Wraps {@link UniversesClient} as a `ResourceDriver<"universe">`. `create`
|
|
3026
|
+
* and `update` both delegate to a shared reconcile helper because Open
|
|
3027
|
+
* Cloud cannot mint universes; the user supplies an existing `universeId`
|
|
3028
|
+
* and bedrock adopts the universe on first apply.
|
|
2744
3029
|
*
|
|
2745
|
-
* A
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
3030
|
+
* A `NotFound` error (HTTP 404) from `UniversesClient.update` is repackaged
|
|
3031
|
+
* as an adoption-error `ApiError` whose message names the config key and
|
|
3032
|
+
* the `universeId`, so operators can tell adoption failure apart from
|
|
3033
|
+
* transient upstream errors. A successful response whose `rootPlaceId` is
|
|
3034
|
+
* absent surfaces as an `ApiError` with status 200, mirroring the
|
|
3035
|
+
* malformed-response guard in `GamePassDriver`.
|
|
3036
|
+
*
|
|
3037
|
+
* When `displayName` is declared, the driver routes that field through
|
|
3038
|
+
* `PlacesClient.update` on the root place after the universe PATCH
|
|
3039
|
+
* succeeds. A subsequent places failure surfaces to the caller as the
|
|
3040
|
+
* driver's error result without rolling back the prior universe patch,
|
|
3041
|
+
* so callers observing a partial failure should reconcile by
|
|
3042
|
+
* reapplying rather than assuming the universe-level fields are
|
|
3043
|
+
* unchanged.
|
|
3044
|
+
*
|
|
3045
|
+
* @param deps - Injected ocale clients (universes plus places for the
|
|
3046
|
+
* read-only universe fields Roblox derives from the root place).
|
|
3047
|
+
* @returns A driver indexable by `"universe"` in a `DriverRegistry`.
|
|
2748
3048
|
*
|
|
2749
3049
|
* @example
|
|
2750
3050
|
*
|
|
2751
3051
|
* ```ts
|
|
2752
|
-
* import {
|
|
3052
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
3053
|
+
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
3054
|
+
* import { UniversesClient } from "@bedrock-rbx/ocale/universes";
|
|
3055
|
+
* import { validUniverseBody } from "@bedrock-rbx/ocale/testing";
|
|
3056
|
+
* import {
|
|
3057
|
+
* asRobloxAssetId,
|
|
3058
|
+
* createUniverseDriver,
|
|
3059
|
+
* UNIVERSE_SINGLETON_KEY,
|
|
3060
|
+
* } from "@bedrock-rbx/core";
|
|
2753
3061
|
*
|
|
2754
|
-
* const
|
|
2755
|
-
*
|
|
2756
|
-
*
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
3062
|
+
* const universeBodyHttpClient: HttpClient = {
|
|
3063
|
+
* async request() {
|
|
3064
|
+
* return {
|
|
3065
|
+
* data: {
|
|
3066
|
+
* body: validUniverseBody({
|
|
3067
|
+
* path: "universes/1234567890",
|
|
3068
|
+
* rootPlace: "universes/1234567890/places/4711",
|
|
3069
|
+
* }),
|
|
3070
|
+
* headers: {},
|
|
3071
|
+
* status: 200,
|
|
3072
|
+
* },
|
|
3073
|
+
* success: true,
|
|
3074
|
+
* };
|
|
3075
|
+
* },
|
|
3076
|
+
* };
|
|
2760
3077
|
*
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
*
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
3078
|
+
* const driver = createUniverseDriver({
|
|
3079
|
+
* places: new PlacesClient({
|
|
3080
|
+
* apiKey: "rbx-your-key",
|
|
3081
|
+
* httpClient: universeBodyHttpClient,
|
|
3082
|
+
* sleep: async () => {},
|
|
3083
|
+
* }),
|
|
3084
|
+
* universes: new UniversesClient({
|
|
3085
|
+
* apiKey: "rbx-your-key",
|
|
3086
|
+
* httpClient: universeBodyHttpClient,
|
|
3087
|
+
* sleep: async () => {},
|
|
3088
|
+
* }),
|
|
3089
|
+
* });
|
|
3090
|
+
*
|
|
3091
|
+
* return driver
|
|
3092
|
+
* .create({
|
|
3093
|
+
* consoleEnabled: undefined,
|
|
3094
|
+
* desktopEnabled: true,
|
|
3095
|
+
* displayName: undefined,
|
|
3096
|
+
* key: UNIVERSE_SINGLETON_KEY,
|
|
3097
|
+
* kind: "universe",
|
|
3098
|
+
* mobileEnabled: undefined,
|
|
3099
|
+
* privateServerPriceRobux: undefined,
|
|
3100
|
+
* tabletEnabled: undefined,
|
|
3101
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
3102
|
+
* voiceChatEnabled: true,
|
|
3103
|
+
* vrEnabled: undefined,
|
|
3104
|
+
* })
|
|
3105
|
+
* .then((result) => {
|
|
3106
|
+
* expect(result.success).toBeTrue();
|
|
3107
|
+
* if (result.success) {
|
|
3108
|
+
* expect(result.data.outputs.rootPlaceId).toBe("4711");
|
|
3109
|
+
* }
|
|
3110
|
+
* });
|
|
3111
|
+
* ```
|
|
2767
3112
|
*/
|
|
2768
|
-
declare function
|
|
3113
|
+
declare function createUniverseDriver(deps: UniverseDriverDeps): ResourceDriver<"universe">;
|
|
2769
3114
|
//#endregion
|
|
2770
|
-
//#region src/
|
|
3115
|
+
//#region src/cli/clack-port.d.ts
|
|
2771
3116
|
/**
|
|
2772
|
-
*
|
|
2773
|
-
*
|
|
2774
|
-
*
|
|
2775
|
-
*
|
|
2776
|
-
*
|
|
2777
|
-
* `diff` runs and before any apply-side driver I/O is attempted.
|
|
2778
|
-
*
|
|
2779
|
-
* Pure and synchronous. Current-only entries (no matching desired) are
|
|
2780
|
-
* ignored: their reconciliation is `diff`'s concern, not this seam's.
|
|
2781
|
-
*
|
|
2782
|
-
* @param desired - Desired state from `buildDesired`.
|
|
2783
|
-
* @param current - Prior current state from the state port.
|
|
2784
|
-
* @returns `Ok(undefined)` when every paired entry passes its kind-level
|
|
2785
|
-
* reconcilability check, or the first `Err` returned by a hook.
|
|
3117
|
+
* Construct a `ClackPort` whose methods delegate to `@clack/prompts`. The
|
|
3118
|
+
* resulting port writes to `process.stdout` via clack's defaults. Kept in
|
|
3119
|
+
* its own module so consumers that never need the clack-backed rendering
|
|
3120
|
+
* (programmatic deploys, custom adapters) do not pull `@clack/prompts`
|
|
3121
|
+
* into their bundle.
|
|
2786
3122
|
*
|
|
2787
3123
|
* @example
|
|
2788
3124
|
*
|
|
2789
3125
|
* ```ts
|
|
2790
|
-
* import {
|
|
3126
|
+
* import { createClackPort } from "@bedrock-rbx/core";
|
|
2791
3127
|
*
|
|
2792
|
-
* const
|
|
2793
|
-
* [
|
|
2794
|
-
* {
|
|
2795
|
-
* description: "Stocks the player up with 1,000 premium gems.",
|
|
2796
|
-
* isRegionalPricingEnabled: undefined,
|
|
2797
|
-
* key: asResourceKey("gem-pack"),
|
|
2798
|
-
* kind: "developerProduct",
|
|
2799
|
-
* name: "Gem Pack",
|
|
2800
|
-
* price: undefined,
|
|
2801
|
-
* storePageEnabled: undefined,
|
|
2802
|
-
* },
|
|
2803
|
-
* ],
|
|
2804
|
-
* [
|
|
2805
|
-
* {
|
|
2806
|
-
* description: "Stocks the player up with 1,000 premium gems.",
|
|
2807
|
-
* icon: { "en-us": "assets/gem-pack.png" },
|
|
2808
|
-
* iconFileHashes: {
|
|
2809
|
-
* "en-us": asSha256Hex(
|
|
2810
|
-
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
2811
|
-
* ),
|
|
2812
|
-
* },
|
|
2813
|
-
* isRegionalPricingEnabled: undefined,
|
|
2814
|
-
* key: asResourceKey("gem-pack"),
|
|
2815
|
-
* kind: "developerProduct",
|
|
2816
|
-
* name: "Gem Pack",
|
|
2817
|
-
* outputs: { productId: asRobloxAssetId("9876543210") },
|
|
2818
|
-
* price: undefined,
|
|
2819
|
-
* storePageEnabled: undefined,
|
|
2820
|
-
* },
|
|
2821
|
-
* ],
|
|
2822
|
-
* );
|
|
3128
|
+
* const port = createClackPort();
|
|
2823
3129
|
*
|
|
2824
|
-
* expect(
|
|
2825
|
-
* if (!result.success) {
|
|
2826
|
-
* expect(result.err.kind).toBe("iconRemovalRejected");
|
|
2827
|
-
* }
|
|
3130
|
+
* expect(typeof port.logSuccess).toBe("function");
|
|
2828
3131
|
* ```
|
|
3132
|
+
*
|
|
3133
|
+
* @returns A port whose six methods each invoke the matching clack helper.
|
|
2829
3134
|
*/
|
|
2830
|
-
declare function
|
|
3135
|
+
declare function createClackPort(): ClackPort;
|
|
2831
3136
|
//#endregion
|
|
2832
|
-
//#region src/
|
|
3137
|
+
//#region src/core/derive-price-fields.d.ts
|
|
2833
3138
|
/**
|
|
2834
|
-
*
|
|
2835
|
-
*
|
|
3139
|
+
* Wire-shape pricing fragment produced by {@link derivePriceFields}: the
|
|
3140
|
+
* `isForSale` flag and an optional numeric `price`. Mirrors the multipart
|
|
3141
|
+
* fields the Open Cloud `developer-products` create and update endpoints
|
|
3142
|
+
* accept for setting Robux pricing.
|
|
3143
|
+
*/
|
|
3144
|
+
interface PriceFields {
|
|
3145
|
+
/** Whether the developer product should be purchasable. */
|
|
3146
|
+
readonly isForSale: boolean;
|
|
3147
|
+
/** Default price in Robux; absent when the product is off-sale. */
|
|
3148
|
+
readonly price?: number;
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* Translate a Mantle-style optional price into the Open Cloud wire shape.
|
|
2836
3152
|
*
|
|
2837
|
-
* `
|
|
2838
|
-
*
|
|
2839
|
-
*
|
|
2840
|
-
*
|
|
3153
|
+
* `desired.price === undefined` (no price declared) becomes
|
|
3154
|
+
* `{ isForSale: false }` and the `price` key is omitted entirely. A defined
|
|
3155
|
+
* price (including `0`) becomes `{ isForSale: true, price }`. Both
|
|
3156
|
+
* `developerProduct` create and update paths share this helper so the
|
|
3157
|
+
* "absent ⇒ off-sale" semantics live in exactly one place.
|
|
3158
|
+
*
|
|
3159
|
+
* @param desired - Object carrying the user-declared `price`.
|
|
3160
|
+
* @returns The wire-shape `{ isForSale, price? }` fragment.
|
|
2841
3161
|
*
|
|
2842
3162
|
* @example
|
|
2843
3163
|
*
|
|
2844
3164
|
* ```ts
|
|
2845
|
-
* import {
|
|
2846
|
-
*
|
|
2847
|
-
* function describe(err: ApplyError): string {
|
|
2848
|
-
* switch (err.kind) {
|
|
2849
|
-
* case "driverFailure": {
|
|
2850
|
-
* return `driver failed for ${err.key}: ${err.cause.message}`;
|
|
2851
|
-
* }
|
|
2852
|
-
* case "updateUnsupported": {
|
|
2853
|
-
* return `update not supported for ${err.key}`;
|
|
2854
|
-
* }
|
|
2855
|
-
* }
|
|
2856
|
-
* }
|
|
2857
|
-
*
|
|
2858
|
-
* const err: ApplyError = {
|
|
2859
|
-
* key: asResourceKey("vip-pass"),
|
|
2860
|
-
* appliedSoFar: [],
|
|
2861
|
-
* kind: "updateUnsupported",
|
|
2862
|
-
* };
|
|
3165
|
+
* import { derivePriceFields } from "@bedrock-rbx/core";
|
|
2863
3166
|
*
|
|
2864
|
-
* expect(
|
|
3167
|
+
* expect(derivePriceFields({ price: undefined })).toStrictEqual({ isForSale: false });
|
|
3168
|
+
* expect(derivePriceFields({ price: 250 })).toStrictEqual({ isForSale: true, price: 250 });
|
|
2865
3169
|
* ```
|
|
2866
3170
|
*/
|
|
2867
|
-
|
|
2868
|
-
readonly
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
} | {
|
|
2873
|
-
readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
|
|
2874
|
-
readonly key: ResourceKey;
|
|
2875
|
-
readonly kind: "updateUnsupported";
|
|
2876
|
-
};
|
|
3171
|
+
declare function derivePriceFields(desired: {
|
|
3172
|
+
readonly price: number | undefined;
|
|
3173
|
+
}): PriceFields;
|
|
3174
|
+
//#endregion
|
|
3175
|
+
//#region src/core/diff.d.ts
|
|
2877
3176
|
/**
|
|
2878
|
-
*
|
|
2879
|
-
*
|
|
2880
|
-
* `updateUnsupported`), the remaining operations are skipped and the error
|
|
2881
|
-
* is returned verbatim.
|
|
3177
|
+
* Computes the operations required to reconcile `current` state with `desired`
|
|
3178
|
+
* state. Pure and synchronous: no I/O, no side effects, no `Result` wrapper.
|
|
2882
3179
|
*
|
|
2883
|
-
*
|
|
2884
|
-
*
|
|
2885
|
-
*
|
|
2886
|
-
*
|
|
2887
|
-
*
|
|
2888
|
-
*
|
|
3180
|
+
* Each entry in `desired` is matched to `current` by `(kind, key)`: resources
|
|
3181
|
+
* are uniquely identified by that pair, so a `place` and a `universe` keyed
|
|
3182
|
+
* `"main"` are independent slots. A `(kind, key)` pair present only in
|
|
3183
|
+
* `desired` produces a `create` op; a pair present in both produces an
|
|
3184
|
+
* `update` op if any declared field differs or a `noop` op if every field
|
|
3185
|
+
* matches.
|
|
2889
3186
|
*
|
|
2890
|
-
*
|
|
2891
|
-
*
|
|
2892
|
-
*
|
|
2893
|
-
* state
|
|
3187
|
+
* Ops appear in the order their desired entries appear in the input array so
|
|
3188
|
+
* callers can rely on declaration order when logging or applying ops.
|
|
3189
|
+
*
|
|
3190
|
+
* @param desired - Declared desired state from user config, already normalized
|
|
3191
|
+
* (file hashes computed, nullable wire values mapped to `undefined`).
|
|
3192
|
+
* @param current - Last-known live state from the state file.
|
|
3193
|
+
* @returns Operations to reconcile the two snapshots.
|
|
2894
3194
|
*
|
|
2895
|
-
* @param ops - Reconciliation operations produced by `diff`, applied in order.
|
|
2896
|
-
* @param registry - Per-kind driver table; dispatch uses `op.desired.kind` as the index.
|
|
2897
|
-
* @returns `Ok(state)` when every operation succeeds, where `state` holds
|
|
2898
|
-
* driver outputs for each non-noop op in dispatched order; or the first
|
|
2899
|
-
* failure encountered.
|
|
2900
|
-
* @throws Whatever the dispatched driver rejects with outside its `Result`
|
|
2901
|
-
* return. A driver whose injected I/O (file reads, network calls, etc.)
|
|
2902
|
-
* throws will surface that rejection here rather than translating it into
|
|
2903
|
-
* a `Result` failure; wrap the call site in a try/catch when drivers are
|
|
2904
|
-
* not trusted to contain their own rejections.
|
|
2905
3195
|
* @example
|
|
2906
3196
|
*
|
|
2907
3197
|
* ```ts
|
|
2908
3198
|
* import {
|
|
2909
|
-
* applyOps,
|
|
2910
3199
|
* asResourceKey,
|
|
2911
3200
|
* asRobloxAssetId,
|
|
2912
3201
|
* asSha256Hex,
|
|
2913
|
-
*
|
|
2914
|
-
* type
|
|
3202
|
+
* diff,
|
|
3203
|
+
* type GamePassDesiredState,
|
|
3204
|
+
* type ResourceCurrentState,
|
|
2915
3205
|
* } from "@bedrock-rbx/core";
|
|
2916
3206
|
*
|
|
2917
|
-
* const
|
|
2918
|
-
*
|
|
2919
|
-
*
|
|
2920
|
-
*
|
|
2921
|
-
*
|
|
2922
|
-
*
|
|
2923
|
-
*
|
|
2924
|
-
*
|
|
2925
|
-
*
|
|
2926
|
-
*
|
|
2927
|
-
*
|
|
2928
|
-
*
|
|
2929
|
-
*
|
|
2930
|
-
*
|
|
2931
|
-
*
|
|
2932
|
-
*
|
|
2933
|
-
*
|
|
2934
|
-
*
|
|
2935
|
-
*
|
|
2936
|
-
*
|
|
2937
|
-
*
|
|
2938
|
-
*
|
|
2939
|
-
* },
|
|
2940
|
-
* universe: {
|
|
2941
|
-
* async create(desired) {
|
|
2942
|
-
* return {
|
|
2943
|
-
* data: { ...desired, outputs: { rootPlaceId: asRobloxAssetId("4711") } },
|
|
2944
|
-
* success: true,
|
|
2945
|
-
* };
|
|
2946
|
-
* },
|
|
2947
|
-
* },
|
|
2948
|
-
* developerProduct: {
|
|
2949
|
-
* async create(desired) {
|
|
2950
|
-
* return {
|
|
2951
|
-
* data: {
|
|
2952
|
-
* ...desired,
|
|
2953
|
-
* outputs: { productId: asRobloxAssetId("8172635495") },
|
|
2954
|
-
* },
|
|
2955
|
-
* success: true,
|
|
2956
|
-
* };
|
|
2957
|
-
* },
|
|
2958
|
-
* },
|
|
3207
|
+
* const hash = asSha256Hex(
|
|
3208
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
3209
|
+
* );
|
|
3210
|
+
*
|
|
3211
|
+
* const unchanged: GamePassDesiredState = {
|
|
3212
|
+
* description: "Grants VIP perks.",
|
|
3213
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
3214
|
+
* iconFileHashes: { "en-us": hash },
|
|
3215
|
+
* key: asResourceKey("vip-pass"),
|
|
3216
|
+
* kind: "gamePass",
|
|
3217
|
+
* name: "VIP Pass",
|
|
3218
|
+
* price: 500,
|
|
3219
|
+
* };
|
|
3220
|
+
* const drifted: GamePassDesiredState = {
|
|
3221
|
+
* ...unchanged,
|
|
3222
|
+
* key: asResourceKey("legend-pass"),
|
|
3223
|
+
* name: "Legend Pass (renamed)",
|
|
3224
|
+
* };
|
|
3225
|
+
* const fresh: GamePassDesiredState = {
|
|
3226
|
+
* ...unchanged,
|
|
3227
|
+
* key: asResourceKey("rookie-pass"),
|
|
3228
|
+
* name: "Rookie Pass",
|
|
2959
3229
|
* };
|
|
2960
3230
|
*
|
|
2961
|
-
* const
|
|
3231
|
+
* const current: ReadonlyArray<ResourceCurrentState> = [
|
|
2962
3232
|
* {
|
|
2963
|
-
*
|
|
2964
|
-
*
|
|
2965
|
-
*
|
|
2966
|
-
*
|
|
2967
|
-
*
|
|
2968
|
-
*
|
|
2969
|
-
*
|
|
2970
|
-
*
|
|
2971
|
-
*
|
|
2972
|
-
*
|
|
2973
|
-
*
|
|
2974
|
-
* },
|
|
2975
|
-
* kind: "gamePass",
|
|
2976
|
-
* price: 500,
|
|
3233
|
+
* ...unchanged,
|
|
3234
|
+
* outputs: {
|
|
3235
|
+
* assetId: asRobloxAssetId("111"),
|
|
3236
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("222") },
|
|
3237
|
+
* },
|
|
3238
|
+
* },
|
|
3239
|
+
* {
|
|
3240
|
+
* ...drifted,
|
|
3241
|
+
* name: "Legend Pass",
|
|
3242
|
+
* outputs: {
|
|
3243
|
+
* assetId: asRobloxAssetId("333"),
|
|
3244
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("444") },
|
|
2977
3245
|
* },
|
|
2978
3246
|
* },
|
|
2979
3247
|
* ];
|
|
2980
3248
|
*
|
|
2981
|
-
*
|
|
2982
|
-
*
|
|
2983
|
-
*
|
|
2984
|
-
* });
|
|
3249
|
+
* const ops = diff([unchanged, drifted, fresh], current);
|
|
3250
|
+
*
|
|
3251
|
+
* expect(ops.map((op) => op.type)).toEqual(["noop", "update", "create"]);
|
|
2985
3252
|
* ```
|
|
2986
3253
|
*/
|
|
2987
|
-
declare function
|
|
3254
|
+
declare function diff(desired: ReadonlyArray<ResourceDesiredState>, current: ReadonlyArray<ResourceCurrentState>): ReadonlyArray<Operation>;
|
|
2988
3255
|
//#endregion
|
|
2989
|
-
//#region src/
|
|
2990
|
-
/**
|
|
2991
|
-
* Failure surfaced when a default-constructed adapter cannot find a
|
|
2992
|
-
* required environment variable. The deploy boundary wraps this in a
|
|
2993
|
-
* `DeployError` so the caller sees a typed Result instead of an
|
|
2994
|
-
* exception or a confusing downstream HTTP error.
|
|
2995
|
-
*/
|
|
2996
|
-
interface MissingCredentialError {
|
|
2997
|
-
/** Literal discriminator for narrowing. */
|
|
2998
|
-
readonly kind: "missingCredential";
|
|
2999
|
-
/** Whether the credential was needed for the state backend or the driver registry. */
|
|
3000
|
-
readonly purpose: "registry" | "stateBackend";
|
|
3001
|
-
/** Environment variable name the default-construction path tried to read. */
|
|
3002
|
-
readonly variable: string;
|
|
3003
|
-
}
|
|
3256
|
+
//#region src/core/display-name-prefix.d.ts
|
|
3004
3257
|
/**
|
|
3005
|
-
*
|
|
3006
|
-
*
|
|
3007
|
-
*
|
|
3258
|
+
* Default template applied when a project enables display-name prefixing
|
|
3259
|
+
* without supplying its own `displayNamePrefix.format`. Yields outputs
|
|
3260
|
+
* like `[STAGING] ` for an environment whose `label` is `"staging"`.
|
|
3008
3261
|
*/
|
|
3009
|
-
|
|
3010
|
-
/** Backend name read from `state.backend`. */
|
|
3011
|
-
readonly backend: string;
|
|
3012
|
-
/** Suggested escape hatch routed back to the caller. */
|
|
3013
|
-
readonly hint: string;
|
|
3014
|
-
/** Literal discriminator for narrowing. */
|
|
3015
|
-
readonly kind: "unsupportedBackend";
|
|
3016
|
-
}
|
|
3017
|
-
/** Inputs for {@link buildStatePort}. */
|
|
3018
|
-
interface BuildStatePortDeps {
|
|
3019
|
-
/** Optional `fetch` seam plumbed through to the gist adapter for tests. */
|
|
3020
|
-
readonly fetch?: GistFetch | undefined;
|
|
3021
|
-
/** Reads an environment variable; injected so tests stay free of `process.env`. */
|
|
3022
|
-
readonly getEnv: (name: string) => string | undefined;
|
|
3023
|
-
/** Resolved state configuration for the target environment. */
|
|
3024
|
-
readonly stateConfig: StateConfig;
|
|
3025
|
-
}
|
|
3262
|
+
declare const DEFAULT_PREFIX_FORMAT = "[{LABEL}] ";
|
|
3026
3263
|
/**
|
|
3027
|
-
*
|
|
3028
|
-
*
|
|
3029
|
-
*
|
|
3030
|
-
*
|
|
3264
|
+
* Render the prefix that selectEnvironment prepends to declared display
|
|
3265
|
+
* names when a project enables `displayNamePrefix`. The template
|
|
3266
|
+
* recognizes three placeholders:
|
|
3267
|
+
*
|
|
3268
|
+
* - `{label}`: label as written.
|
|
3269
|
+
* - `{LABEL}`: upper-cased label.
|
|
3270
|
+
* - `{Label}`: capitalized label (first character upper, rest as written).
|
|
3271
|
+
*
|
|
3272
|
+
* Other characters in the template flow through verbatim.
|
|
3273
|
+
*
|
|
3274
|
+
* @param label - Environment label declared on `EnvironmentEntry.label`.
|
|
3275
|
+
* @param format - Template string. Falls back to
|
|
3276
|
+
* {@link DEFAULT_PREFIX_FORMAT} when omitted.
|
|
3277
|
+
* @returns The rendered prefix string.
|
|
3031
3278
|
*
|
|
3032
3279
|
* @example
|
|
3033
3280
|
*
|
|
3034
3281
|
* ```ts
|
|
3035
|
-
* import {
|
|
3036
|
-
*
|
|
3037
|
-
* const port = buildStatePort({
|
|
3038
|
-
* fetch: async () =>
|
|
3039
|
-
* new Response(JSON.stringify({ files: {} }), { status: 200 }),
|
|
3040
|
-
* getEnv: (name) => (name === "GITHUB_TOKEN" ? "ghp_example" : undefined),
|
|
3041
|
-
* stateConfig: { backend: "gist", gistId: "abc123" },
|
|
3042
|
-
* });
|
|
3282
|
+
* import { renderDisplayNamePrefix } from "@bedrock-rbx/core";
|
|
3043
3283
|
*
|
|
3044
|
-
* expect(
|
|
3284
|
+
* expect(renderDisplayNamePrefix("staging")).toBe("[STAGING] ");
|
|
3285
|
+
* expect(renderDisplayNamePrefix("staging", "{Label}: ")).toBe("Staging: ");
|
|
3286
|
+
* expect(renderDisplayNamePrefix("dev", "{LABEL}-{label}")).toBe("DEV-dev");
|
|
3045
3287
|
* ```
|
|
3046
|
-
*
|
|
3047
|
-
* @param deps - Resolved state config plus credential-injection seams.
|
|
3048
|
-
* @returns A `StatePort` on success, or a typed Err describing the
|
|
3049
|
-
* missing credential or the unsupported backend.
|
|
3050
3288
|
*/
|
|
3051
|
-
declare function
|
|
3289
|
+
declare function renderDisplayNamePrefix(label: string, format?: string): string;
|
|
3052
3290
|
//#endregion
|
|
3053
|
-
//#region src/
|
|
3054
|
-
/**
|
|
3055
|
-
* Failure surfaced when default-constructing a registry needs a config
|
|
3056
|
-
* field that is not present. The deploy boundary wraps this in a
|
|
3057
|
-
* `DeployError` so the caller sees a typed Result instead of a downstream
|
|
3058
|
-
* driver error.
|
|
3059
|
-
*/
|
|
3060
|
-
interface RegistryConfigError {
|
|
3061
|
-
/** Suggested fix routed back to the caller. */
|
|
3062
|
-
readonly hint: string;
|
|
3063
|
-
/** Literal discriminator for narrowing. */
|
|
3064
|
-
readonly kind: "registryConfigMissing";
|
|
3065
|
-
/** Which config field was missing. */
|
|
3066
|
-
readonly missing: "universeId";
|
|
3067
|
-
}
|
|
3068
|
-
/** Inputs for {@link buildDefaultRegistry}. */
|
|
3069
|
-
interface BuildDefaultRegistryDeps {
|
|
3070
|
-
/** Resolved project config; supplies `universe.universeId` and is read for nothing else. */
|
|
3071
|
-
readonly config: ResolvedConfig;
|
|
3072
|
-
/** Reads an environment variable; injected so tests stay free of `process.env`. */
|
|
3073
|
-
readonly getEnv: (name: string) => string | undefined;
|
|
3074
|
-
/** Reader plumbed into kind-specific drivers that ingest file bytes. */
|
|
3075
|
-
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
3076
|
-
}
|
|
3291
|
+
//#region src/core/environment.d.ts
|
|
3077
3292
|
/**
|
|
3078
|
-
*
|
|
3079
|
-
*
|
|
3080
|
-
*
|
|
3081
|
-
*
|
|
3293
|
+
* Validate an environment name at a state-adapter boundary.
|
|
3294
|
+
*
|
|
3295
|
+
* Adapters that map environment names onto filesystem-like identifiers
|
|
3296
|
+
* (gist filenames, S3 keys) must reject names that could collide or escape
|
|
3297
|
+
* their storage layout. This helper accepts letters, digits, `-`, and `_`
|
|
3298
|
+
* only, with length between 1 and 64, and returns a `StateError` for
|
|
3299
|
+
* anything outside that set so the adapter can fail loudly instead of
|
|
3300
|
+
* silently stripping characters.
|
|
3082
3301
|
*
|
|
3083
3302
|
* @example
|
|
3084
3303
|
*
|
|
3085
3304
|
* ```ts
|
|
3086
|
-
* import {
|
|
3305
|
+
* import { validateEnvironmentName } from "@bedrock-rbx/core";
|
|
3087
3306
|
*
|
|
3088
|
-
* const
|
|
3089
|
-
*
|
|
3090
|
-
* environments: { production: {} },
|
|
3091
|
-
* state: { backend: "gist", gistId: "abc" },
|
|
3092
|
-
* universe: { universeId: "1234567890" },
|
|
3093
|
-
* },
|
|
3094
|
-
* getEnv: () => "rbx-test",
|
|
3095
|
-
* readFile: async () => new Uint8Array(),
|
|
3096
|
-
* });
|
|
3307
|
+
* const ok = validateEnvironmentName("production");
|
|
3308
|
+
* expect(ok.success).toBeTrue();
|
|
3097
3309
|
*
|
|
3098
|
-
*
|
|
3310
|
+
* const bad = validateEnvironmentName("prod/staging");
|
|
3311
|
+
* expect(bad.success).toBeFalse();
|
|
3099
3312
|
* ```
|
|
3100
3313
|
*
|
|
3101
|
-
* @param
|
|
3102
|
-
* @returns
|
|
3103
|
-
*
|
|
3314
|
+
* @param environment - Raw environment name supplied by a caller.
|
|
3315
|
+
* @returns `Ok(environment)` when the name is safe to use, or
|
|
3316
|
+
* `Err(StateError)` with a descriptive reason when it is not.
|
|
3104
3317
|
*/
|
|
3105
|
-
declare function
|
|
3318
|
+
declare function validateEnvironmentName(environment: string): Result$1<string, StateError>;
|
|
3106
3319
|
//#endregion
|
|
3107
|
-
//#region src/
|
|
3320
|
+
//#region src/core/get-environment.d.ts
|
|
3108
3321
|
/**
|
|
3109
|
-
*
|
|
3110
|
-
|
|
3322
|
+
* Failure modes returned by {@link getEnvironment}.
|
|
3323
|
+
*/
|
|
3324
|
+
type GetEnvironmentError = {
|
|
3325
|
+
readonly kind: "missingEnvironment";
|
|
3326
|
+
} | {
|
|
3327
|
+
readonly kind: "multipleEnvironments";
|
|
3328
|
+
readonly values: ReadonlyArray<string>;
|
|
3329
|
+
};
|
|
3330
|
+
/**
|
|
3331
|
+
* Resolve the deploy environment for an override script invocation.
|
|
3111
3332
|
*
|
|
3112
|
-
*
|
|
3113
|
-
*
|
|
3114
|
-
*
|
|
3115
|
-
*
|
|
3333
|
+
* Reads `--env <name>` from the supplied argv first, falls back to
|
|
3334
|
+
* `BEDROCK_ENVIRONMENT` from the supplied env reader. Returns
|
|
3335
|
+
* `missingEnvironment` when neither is present and `multipleEnvironments`
|
|
3336
|
+
* (with every offending value) when argv contains more than one `--env`
|
|
3337
|
+
* flag. Both inputs default to the running process so override scripts
|
|
3338
|
+
* under `.bedrock/` can call `getEnvironment()` with no arguments.
|
|
3116
3339
|
*
|
|
3117
|
-
* @param
|
|
3118
|
-
*
|
|
3119
|
-
*
|
|
3120
|
-
*
|
|
3121
|
-
*
|
|
3340
|
+
* @param argv - Argument list to scan for `--env <name>` flags. Defaults to
|
|
3341
|
+
* `process.argv.slice(2)` when omitted.
|
|
3342
|
+
* @param readEnvironment - Reads an environment variable; consulted as a
|
|
3343
|
+
* fallback when no `--env` flag is present. Defaults to a `process.env`
|
|
3344
|
+
* reader when omitted.
|
|
3345
|
+
* @returns `Ok(environment)` on success, `Err(GetEnvironmentError)` otherwise.
|
|
3122
3346
|
* @example
|
|
3123
3347
|
*
|
|
3124
3348
|
* ```ts
|
|
3125
|
-
* import {
|
|
3349
|
+
* import { getEnvironment } from "@bedrock-rbx/core";
|
|
3126
3350
|
*
|
|
3127
|
-
*
|
|
3128
|
-
* return new Uint8Array([1, 2, 3]);
|
|
3129
|
-
* }
|
|
3351
|
+
* const result = getEnvironment(["--env", "production"], () => undefined);
|
|
3130
3352
|
*
|
|
3131
|
-
*
|
|
3132
|
-
*
|
|
3133
|
-
*
|
|
3134
|
-
*
|
|
3135
|
-
* icon: { "en-us": "assets/vip-icon.png" },
|
|
3136
|
-
* key: asResourceKey("vip-pass"),
|
|
3137
|
-
* kind: "gamePass",
|
|
3138
|
-
* name: "VIP Pass",
|
|
3139
|
-
* price: 500,
|
|
3140
|
-
* },
|
|
3141
|
-
* ],
|
|
3142
|
-
* readFile,
|
|
3143
|
-
* ).then((result) => {
|
|
3144
|
-
* expect(result.success).toBeTrue();
|
|
3145
|
-
* if (result.success) {
|
|
3146
|
-
* expect(result.data).toHaveLength(1);
|
|
3147
|
-
* expect(result.data[0]!.kind).toBe("gamePass");
|
|
3148
|
-
* }
|
|
3149
|
-
* });
|
|
3353
|
+
* expect(result.success).toBeTrue();
|
|
3354
|
+
* if (result.success) {
|
|
3355
|
+
* expect(result.data).toBe("production");
|
|
3356
|
+
* }
|
|
3150
3357
|
* ```
|
|
3151
3358
|
*/
|
|
3152
|
-
declare function
|
|
3359
|
+
declare function getEnvironment(argv?: ReadonlyArray<string>, readEnvironment?: (name: string) => string | undefined): Result$1<string, GetEnvironmentError>;
|
|
3153
3360
|
//#endregion
|
|
3154
|
-
//#region src/
|
|
3155
|
-
/**
|
|
3156
|
-
* Options for {@link loadConfig}. Matches a subset of c12's loader options;
|
|
3157
|
-
* additional fields land with the issues that introduce each flow.
|
|
3158
|
-
*/
|
|
3159
|
-
interface LoadConfigOptions {
|
|
3160
|
-
/**
|
|
3161
|
-
* Path to a specific config file to load, including its extension.
|
|
3162
|
-
* Resolved relative to `cwd` when not absolute. Loaded as-is with no
|
|
3163
|
-
* extension search; if the file does not exist at the given path,
|
|
3164
|
-
* `loadConfig` returns `fileNotFound`. When omitted, `loadConfig`
|
|
3165
|
-
* discovers `bedrock.config.{ts,js,...}` from `cwd`.
|
|
3166
|
-
*/
|
|
3167
|
-
readonly configFile?: string;
|
|
3168
|
-
/**
|
|
3169
|
-
* Directory to search from. Defaults to `process.cwd()` at call time, so
|
|
3170
|
-
* each invocation sees the current working directory.
|
|
3171
|
-
*/
|
|
3172
|
-
readonly cwd?: string;
|
|
3173
|
-
}
|
|
3361
|
+
//#region src/core/icons.d.ts
|
|
3174
3362
|
/**
|
|
3175
|
-
*
|
|
3176
|
-
*
|
|
3177
|
-
*
|
|
3178
|
-
*
|
|
3179
|
-
*
|
|
3180
|
-
* long-running scripts see up-to-date values.
|
|
3363
|
+
* Cost-gate for icon re-uploads. Returns `true` when the locally-hashed
|
|
3364
|
+
* desired icon differs from the hash recorded on the prior current-state
|
|
3365
|
+
* entry, signalling that the driver must re-upload before reconciling.
|
|
3366
|
+
* Returns `false` when the hashes match (no re-upload needed) and when
|
|
3367
|
+
* both sides report no icon.
|
|
3181
3368
|
*
|
|
3182
|
-
*
|
|
3183
|
-
*
|
|
3184
|
-
*
|
|
3369
|
+
* The signature takes hash maps directly (not whole-state) so the helper
|
|
3370
|
+
* is independent of any specific resource-kind shape; every icon-bearing
|
|
3371
|
+
* driver projects its own `iconFileHashes` and `outputs.iconFileHashes`
|
|
3372
|
+
* fields before calling.
|
|
3185
3373
|
*
|
|
3186
|
-
*
|
|
3187
|
-
*
|
|
3188
|
-
*
|
|
3189
|
-
*
|
|
3190
|
-
* - `validationFailed` - a file was found and parsed, but its content did
|
|
3191
|
-
* not satisfy the runtime schema.
|
|
3192
|
-
* - `configFunctionFailed` - a function-form config threw or its returned
|
|
3193
|
-
* promise rejected while being invoked.
|
|
3374
|
+
* @param currentHashes - Hashes recorded on the prior current-state entry.
|
|
3375
|
+
* @param desiredHashes - Hashes layered onto the desired-state entry by
|
|
3376
|
+
* `normalize` from the local icon file's bytes.
|
|
3377
|
+
* @returns `true` when the driver should re-upload the icon.
|
|
3194
3378
|
*
|
|
3195
|
-
* @param options - Loader options.
|
|
3196
|
-
* @returns `Ok` with the validated `Config`, or `Err` with a `ConfigError`.
|
|
3197
3379
|
* @example
|
|
3198
3380
|
*
|
|
3199
3381
|
* ```ts
|
|
3200
|
-
* import {
|
|
3382
|
+
* import { asSha256Hex, shouldReuploadIcon } from "@bedrock-rbx/core";
|
|
3201
3383
|
*
|
|
3202
|
-
*
|
|
3203
|
-
*
|
|
3204
|
-
*
|
|
3205
|
-
*
|
|
3206
|
-
*
|
|
3207
|
-
*
|
|
3208
|
-
*
|
|
3209
|
-
*
|
|
3210
|
-
* });
|
|
3384
|
+
* const previous = asSha256Hex(
|
|
3385
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
3386
|
+
* );
|
|
3387
|
+
* const fresh = asSha256Hex(
|
|
3388
|
+
* "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
|
|
3389
|
+
* );
|
|
3390
|
+
*
|
|
3391
|
+
* expect(shouldReuploadIcon({ "en-us": previous }, { "en-us": previous })).toBe(false);
|
|
3392
|
+
* expect(shouldReuploadIcon({ "en-us": previous }, { "en-us": fresh })).toBe(true);
|
|
3211
3393
|
* ```
|
|
3212
3394
|
*/
|
|
3213
|
-
declare function
|
|
3395
|
+
declare function shouldReuploadIcon(currentHashes: Record<"en-us", Sha256Hex> | undefined, desiredHashes: Record<"en-us", Sha256Hex> | undefined): boolean;
|
|
3214
3396
|
//#endregion
|
|
3215
|
-
//#region src/
|
|
3397
|
+
//#region src/core/kinds/index.d.ts
|
|
3216
3398
|
/**
|
|
3217
|
-
*
|
|
3218
|
-
*
|
|
3219
|
-
*
|
|
3399
|
+
* Default {@link KindRegistry} composing every resource kind bedrock ships
|
|
3400
|
+
* out of the box. Iteration order (`gamePass`, `place`, `universe`,
|
|
3401
|
+
* `developerProduct`) matches the order `flattenConfig` emits entries
|
|
3402
|
+
* today, preserving the observable order of generated operations.
|
|
3403
|
+
*
|
|
3404
|
+
* @example
|
|
3405
|
+
*
|
|
3406
|
+
* ```ts
|
|
3407
|
+
* import { defaultKindRegistry } from "@bedrock-rbx/core";
|
|
3408
|
+
*
|
|
3409
|
+
* expect(defaultKindRegistry.gamePass.kind).toBe("gamePass");
|
|
3410
|
+
* expect(defaultKindRegistry.place.kind).toBe("place");
|
|
3411
|
+
* expect(defaultKindRegistry.universe.kind).toBe("universe");
|
|
3412
|
+
* expect(defaultKindRegistry.developerProduct.kind).toBe("developerProduct");
|
|
3413
|
+
* ```
|
|
3220
3414
|
*/
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
/** Environment name; threaded into `StatePort.read` and the persisted snapshot. */
|
|
3225
|
-
readonly environment: string;
|
|
3226
|
-
/** `fetch` override plumbed into the default-constructed gist adapter when `statePort` is omitted. */
|
|
3227
|
-
readonly fetch?: GistFetch;
|
|
3228
|
-
/** Reads an environment variable; defaults to `(name) => process.env[name]`. */
|
|
3229
|
-
readonly getEnv?: (name: string) => string | undefined;
|
|
3230
|
-
/** Loader invoked when `config` is omitted; defaults to `loadConfig` from this package. */
|
|
3231
|
-
readonly loadConfig?: (options?: LoadConfigOptions) => Promise<Result$1<Config, ConfigError>>;
|
|
3232
|
-
/** Reads file bytes for resources that have file-backed inputs. Defaults to `node:fs/promises.readFile`. */
|
|
3233
|
-
readonly readFile?: (path: string) => Promise<Uint8Array>;
|
|
3234
|
-
/** Per-kind driver table consulted for create / update dispatch. Default-constructed from `BEDROCK_API_KEY` when omitted. */
|
|
3235
|
-
readonly registry?: DriverRegistry;
|
|
3236
|
-
/** Backend used to read the prior snapshot and persist the new one. Default-constructed from `config.state` and `GITHUB_TOKEN` when omitted. */
|
|
3237
|
-
readonly statePort?: StatePort;
|
|
3238
|
-
}
|
|
3415
|
+
declare const defaultKindRegistry: KindRegistry;
|
|
3416
|
+
//#endregion
|
|
3417
|
+
//#region src/core/state-file.d.ts
|
|
3239
3418
|
/**
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3242
|
-
*
|
|
3243
|
-
*
|
|
3244
|
-
*
|
|
3245
|
-
*
|
|
3419
|
+
* Serialize a {@link BedrockState} to the on-disk JSON representation used by
|
|
3420
|
+
* state-port adapters.
|
|
3421
|
+
*
|
|
3422
|
+
* The on-disk shape wraps the in-memory state with a
|
|
3423
|
+
* `$bedrock: { version: N }` envelope so that a future breaking change to the
|
|
3424
|
+
* schema can be detected and rejected at parse time rather than silently
|
|
3425
|
+
* accepted. The top-level `version` field is not duplicated on disk.
|
|
3426
|
+
*
|
|
3427
|
+
* @example
|
|
3428
|
+
*
|
|
3429
|
+
* ```ts
|
|
3430
|
+
* import { serializeStateFile, type BedrockState } from "@bedrock-rbx/core";
|
|
3431
|
+
*
|
|
3432
|
+
* const state: BedrockState = {
|
|
3433
|
+
* environment: "production",
|
|
3434
|
+
* resources: [],
|
|
3435
|
+
* version: 1,
|
|
3436
|
+
* };
|
|
3437
|
+
*
|
|
3438
|
+
* const wire = serializeStateFile(state);
|
|
3439
|
+
* expect(JSON.parse(wire)).toStrictEqual({
|
|
3440
|
+
* $bedrock: { version: 1 },
|
|
3441
|
+
* environment: "production",
|
|
3442
|
+
* resources: [],
|
|
3443
|
+
* });
|
|
3444
|
+
* ```
|
|
3445
|
+
*
|
|
3446
|
+
* @param state - The in-memory state snapshot to serialize.
|
|
3447
|
+
* @returns A pretty-printed JSON string ready to hand to a state adapter's write method.
|
|
3246
3448
|
*/
|
|
3247
|
-
|
|
3248
|
-
readonly cause: ApplyError;
|
|
3249
|
-
readonly kind: "applyFailed";
|
|
3250
|
-
} | {
|
|
3251
|
-
readonly cause: BuildDesiredError;
|
|
3252
|
-
readonly kind: "buildDesiredFailed";
|
|
3253
|
-
} | {
|
|
3254
|
-
readonly cause: ConfigError;
|
|
3255
|
-
readonly kind: "configLoadFailed";
|
|
3256
|
-
} | {
|
|
3257
|
-
readonly cause: StateError;
|
|
3258
|
-
readonly kind: "stateReadFailed";
|
|
3259
|
-
} | {
|
|
3260
|
-
readonly cause: StateError;
|
|
3261
|
-
readonly kind: "stateWriteFailed";
|
|
3262
|
-
readonly unsavedState: BedrockState;
|
|
3263
|
-
};
|
|
3449
|
+
declare function serializeStateFile(state: BedrockState): string;
|
|
3264
3450
|
/**
|
|
3265
|
-
*
|
|
3266
|
-
*
|
|
3267
|
-
*
|
|
3268
|
-
* `
|
|
3451
|
+
* Parse a raw on-disk state file into a {@link BedrockState}.
|
|
3452
|
+
*
|
|
3453
|
+
* A backend that reports "no state file for this environment yet" must pass
|
|
3454
|
+
* `undefined`: that distinguishes a legitimate first deploy from a file that
|
|
3455
|
+
* exists but cannot be trusted.
|
|
3269
3456
|
*
|
|
3270
|
-
* @param options - Target environment plus optional overrides.
|
|
3271
|
-
* @returns The persisted `BedrockState` on success, or a stage-tagged
|
|
3272
|
-
* `DeployError` on failure.
|
|
3273
3457
|
* @example
|
|
3274
3458
|
*
|
|
3275
3459
|
* ```ts
|
|
3276
|
-
* import {
|
|
3460
|
+
* import { parseStateFile } from "@bedrock-rbx/core";
|
|
3277
3461
|
*
|
|
3278
|
-
*
|
|
3279
|
-
*
|
|
3280
|
-
*
|
|
3281
|
-
*
|
|
3282
|
-
*
|
|
3283
|
-
* });
|
|
3462
|
+
* const freshStart = parseStateFile(undefined, "gist:abc123/state.production.json");
|
|
3463
|
+
* expect(freshStart.success).toBeTrue();
|
|
3464
|
+
* if (freshStart.success) {
|
|
3465
|
+
* expect(freshStart.data).toBeUndefined();
|
|
3466
|
+
* }
|
|
3284
3467
|
* ```
|
|
3285
3468
|
*
|
|
3469
|
+
* @param raw - Raw file contents as a string, or `undefined` when the
|
|
3470
|
+
* backend reports no file exists yet.
|
|
3471
|
+
* @param file - Adapter-specific identifier included in any `StateError`
|
|
3472
|
+
* surfaced during parsing.
|
|
3473
|
+
* @returns `Ok(undefined)` for a missing file, `Ok(state)` for a parseable
|
|
3474
|
+
* file, or `Err(StateError)` for anything that cannot be trusted.
|
|
3475
|
+
*/
|
|
3476
|
+
declare function parseStateFile(raw: string | undefined, file: string): Result$1<BedrockState | undefined, StateError>;
|
|
3477
|
+
//#endregion
|
|
3478
|
+
//#region src/core/validate-plan.d.ts
|
|
3479
|
+
/**
|
|
3480
|
+
* Plan-time invariant check that runs after `buildDesired` and before
|
|
3481
|
+
* `diff`. Walks paired `(kind, key)` entries and dispatches to each
|
|
3482
|
+
* kind module's optional `assertReconcilable` hook so kind-specific
|
|
3483
|
+
* rejections (e.g. Removing a developer-product icon, which the upstream
|
|
3484
|
+
* API has no documented unset path for) surface as typed errors before
|
|
3485
|
+
* `diff` runs and before any apply-side driver I/O is attempted.
|
|
3486
|
+
*
|
|
3487
|
+
* Pure and synchronous. Current-only entries (no matching desired) are
|
|
3488
|
+
* ignored: their reconciliation is `diff`'s concern, not this seam's.
|
|
3489
|
+
*
|
|
3490
|
+
* @param desired - Desired state from `buildDesired`.
|
|
3491
|
+
* @param current - Prior current state from the state port.
|
|
3492
|
+
* @returns `Ok(undefined)` when every paired entry passes its kind-level
|
|
3493
|
+
* reconcilability check, or the first `Err` returned by a hook.
|
|
3494
|
+
*
|
|
3286
3495
|
* @example
|
|
3287
3496
|
*
|
|
3288
3497
|
* ```ts
|
|
3289
|
-
* import {
|
|
3498
|
+
* import { asResourceKey, asRobloxAssetId, asSha256Hex, validatePlan } from "@bedrock-rbx/core";
|
|
3290
3499
|
*
|
|
3291
|
-
* const
|
|
3292
|
-
*
|
|
3293
|
-
*
|
|
3294
|
-
*
|
|
3295
|
-
*
|
|
3296
|
-
*
|
|
3297
|
-
*
|
|
3298
|
-
*
|
|
3299
|
-
*
|
|
3300
|
-
*
|
|
3301
|
-
*
|
|
3302
|
-
*
|
|
3303
|
-
*
|
|
3304
|
-
*
|
|
3305
|
-
*
|
|
3306
|
-
*
|
|
3307
|
-
*
|
|
3308
|
-
*
|
|
3500
|
+
* const result = validatePlan(
|
|
3501
|
+
* [
|
|
3502
|
+
* {
|
|
3503
|
+
* description: "Stocks the player up with 1,000 premium gems.",
|
|
3504
|
+
* isRegionalPricingEnabled: undefined,
|
|
3505
|
+
* key: asResourceKey("gem-pack"),
|
|
3506
|
+
* kind: "developerProduct",
|
|
3507
|
+
* name: "Gem Pack",
|
|
3508
|
+
* price: undefined,
|
|
3509
|
+
* storePageEnabled: undefined,
|
|
3510
|
+
* },
|
|
3511
|
+
* ],
|
|
3512
|
+
* [
|
|
3513
|
+
* {
|
|
3514
|
+
* description: "Stocks the player up with 1,000 premium gems.",
|
|
3515
|
+
* icon: { "en-us": "assets/gem-pack.png" },
|
|
3516
|
+
* iconFileHashes: {
|
|
3517
|
+
* "en-us": asSha256Hex(
|
|
3518
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
3519
|
+
* ),
|
|
3520
|
+
* },
|
|
3521
|
+
* isRegionalPricingEnabled: undefined,
|
|
3522
|
+
* key: asResourceKey("gem-pack"),
|
|
3523
|
+
* kind: "developerProduct",
|
|
3524
|
+
* name: "Gem Pack",
|
|
3525
|
+
* outputs: { productId: asRobloxAssetId("9876543210") },
|
|
3526
|
+
* price: undefined,
|
|
3527
|
+
* storePageEnabled: undefined,
|
|
3528
|
+
* },
|
|
3529
|
+
* ],
|
|
3530
|
+
* );
|
|
3309
3531
|
*
|
|
3310
|
-
*
|
|
3311
|
-
*
|
|
3312
|
-
*
|
|
3313
|
-
*
|
|
3314
|
-
* passes: {},
|
|
3315
|
-
* },
|
|
3316
|
-
* environment: "production",
|
|
3317
|
-
* registry,
|
|
3318
|
-
* statePort,
|
|
3319
|
-
* }).then((result) => {
|
|
3320
|
-
* expect(result.success).toBeTrue();
|
|
3321
|
-
* if (result.success) {
|
|
3322
|
-
* expect(result.data.environment).toBe("production");
|
|
3323
|
-
* expect(result.data.resources).toBeEmpty();
|
|
3324
|
-
* }
|
|
3325
|
-
* });
|
|
3532
|
+
* expect(result.success).toBeFalse();
|
|
3533
|
+
* if (!result.success) {
|
|
3534
|
+
* expect(result.err.kind).toBe("iconRemovalRejected");
|
|
3535
|
+
* }
|
|
3326
3536
|
* ```
|
|
3327
3537
|
*/
|
|
3328
|
-
declare function
|
|
3538
|
+
declare function validatePlan(desired: ReadonlyArray<ResourceDesiredState>, current: ReadonlyArray<ResourceCurrentState>): Result$1<undefined, BuildDesiredError>;
|
|
3329
3539
|
//#endregion
|
|
3330
3540
|
//#region src/shell/migrate-mantle-state.d.ts
|
|
3331
3541
|
type ConfigFormat = "typescript" | "yaml";
|
|
@@ -3422,5 +3632,5 @@ interface MigrateMantleStateDeps {
|
|
|
3422
3632
|
*/
|
|
3423
3633
|
declare function migrateMantleState(deps: MigrateMantleStateDeps): Promise<Result$1<MigrationReport, MigrateError>>;
|
|
3424
3634
|
//#endregion
|
|
3425
|
-
export { type ApplyError, type BaseOperation, type BedrockState, type BuildDesiredError, type Config, type ConfigContext, type ConfigEnvironmentUniverseId, type ConfigError, type ConfigInput, type ConfigRootUniverseId, type ConfigValidationIssue, type CreateOperation, DEFAULT_PREFIX_FORMAT, type DeployError, type DeployOptions, type DeveloperProductDesiredInput, type DeveloperProductDesiredState, type DeveloperProductDriverDeps, type DeveloperProductEntry, type DeveloperProductOutputs, type DisplayNamePrefixConfig, type DriverRegistry, type EnvironmentEntry, type GamePassDesiredInput, type GamePassDesiredState, type GamePassDriverDeps, type GamePassEntry, type GamePassOutputs, type GetEnvironmentError, type GistStateAdapterDeps, type GistStateConfig, type IncompletePlaceEntryError, type IncompleteUniverseEntryError, type KindIo, type KindRegistry, type LoadConfigOptions, type MigrateError, type MigrateMantleStateDeps, type MigrationReport, type MigrationSummary, type MigrationWarning, type MissingCredentialError, type NoopOperation, OpenCloudError, type Operation, type PlaceDesiredInput, type PlaceDesiredState, type PlaceDriverDeps, type PlaceEntry, type PlaceOutputs, type PriceFields, type RegistryConfigError, type ResolvedConfig, type ResolvedPlaceEntry, type ResolvedUniverseEntry, type ResourceCurrentState, type ResourceDesiredInput, type ResourceDesiredState, type ResourceDriver, type ResourceEntryByKind, type ResourceKey, type ResourceKind, type ResourceKindModule, type ResourceOutputs, type ResourceOutputsByKind, type Result, type RobloxAssetId, SOCIAL_LINK_FIELDS, type SelectEnvironmentError, type Sha256Hex, type SocialLink, type SocialLinkField, type StateConfig, type StateError, type StateNotConfiguredError, type StatePort, type StatesByEnvironment, UNIVERSE_SINGLETON_KEY, type UniverseDesiredInput, type UniverseDesiredState, type UniverseDriverDeps, type UniverseEntry, type UniverseOutputs, type UniverseOverlayWithId, type UniverseOverlayWithoutId, type UnknownEnvironmentError, type UnsupportedBackendError, type UpdateOperation, applyOps, asResourceKey, asRobloxAssetId, asSha256Hex, buildDefaultRegistry, buildDesired, buildStatePort, createDeveloperProductDriver, createGamePassDriver, createGistStateAdapter, createPlaceDriver, createUniverseDriver, defaultKindRegistry, defineConfig, deploy, derivePriceFields, diff, flattenConfig, getEnvironment, isGistStateConfig, isResourceKey, isRobloxAssetId, isSha256Hex, loadConfig, migrateMantleState, parseStateFile, renderDisplayNamePrefix, resolveStateConfig, selectEnvironment, serializeStateFile, shouldReuploadIcon, validateConfig, validateEnvironmentName, validatePlan };
|
|
3635
|
+
export { type ApplyError, type BaseOperation, type BedrockState, type BuildDesiredError, type ClackPort, type ClackProgressAdapterDeps, type Config, type ConfigContext, type ConfigEnvironmentUniverseId, type ConfigError, type ConfigInput, type ConfigRootUniverseId, type ConfigValidationIssue, type CreateOperation, DEFAULT_PREFIX_FORMAT, type DeployError, type DeployFailureEvent, type DeployOptions, type DeploySuccessEvent, type DeveloperProductDesiredInput, type DeveloperProductDesiredState, type DeveloperProductDriverDeps, type DeveloperProductEntry, type DeveloperProductOutputs, type DisplayNamePrefixConfig, type DriverRegistry, type EnvironmentEntry, type GamePassDesiredInput, type GamePassDesiredState, type GamePassDriverDeps, type GamePassEntry, type GamePassOutputs, type GetEnvironmentError, type GistStateAdapterDeps, type GistStateConfig, type IncompletePlaceEntryError, type IncompleteUniverseEntryError, type KindIo, type KindRegistry, type LoadConfigOptions, type MigrateError, type MigrateMantleStateDeps, type MigrationReport, type MigrationSummary, type MigrationWarning, type MissingCredentialError, type NoopOperation, OpenCloudError, type Operation, type PlaceDesiredInput, type PlaceDesiredState, type PlaceDriverDeps, type PlaceEntry, type PlaceOutputs, type PriceFields, type ProgressEvent, type ProgressPort, type RedactedDeveloperProductOverride, type RedactedGamePassOverride, type RedactedPlaceOverride, type RegistryConfigError, type ResolvedConfig, type ResolvedPlaceEntry, type ResolvedUniverseEntry, type ResourceCurrentState, type ResourceDesiredInput, type ResourceDesiredState, type ResourceDriver, type ResourceEntryByKind, type ResourceKey, type ResourceKind, type ResourceKindModule, type ResourceOutputs, type ResourceOutputsByKind, type Result, type RobloxAssetId, SOCIAL_LINK_FIELDS, type SelectEnvironmentError, type Sha256Hex, type SocialLink, type SocialLinkField, type StateConfig, type StateError, type StateNotConfiguredError, type StatePort, type StatesByEnvironment, UNIVERSE_SINGLETON_KEY, type UniverseDesiredInput, type UniverseDesiredState, type UniverseDriverDeps, type UniverseEntry, type UniverseOutputs, type UniverseOverlayWithId, type UniverseOverlayWithoutId, type UnknownEnvironmentError, type UnsupportedBackendError, type UpdateOperation, applyOps, asResourceKey, asRobloxAssetId, asSha256Hex, buildDefaultRegistry, buildDesired, buildStatePort, createClackPort, createClackProgressAdapter, createDeveloperProductDriver, createGamePassDriver, createGistStateAdapter, createNoOpProgressAdapter, createPlaceDriver, createUniverseDriver, defaultKindRegistry, defineConfig, deploy, derivePriceFields, diff, flattenConfig, getEnvironment, isGistStateConfig, isResourceKey, isRobloxAssetId, isSha256Hex, loadConfig, migrateMantleState, parseStateFile, renderDisplayNamePrefix, resolveStateConfig, selectEnvironment, serializeStateFile, shouldReuploadIcon, validateConfig, validateEnvironmentName, validatePlan };
|
|
3426
3636
|
//# sourceMappingURL=index.d.mts.map
|