@bedrock-rbx/core 0.1.0-beta.11 → 0.1.0-beta.12
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 +17 -204
- package/dist/cli/run.mjs.map +1 -1
- package/dist/index.d.mts +1827 -1638
- 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-Dkk5zGHw.mjs} +318 -15
- package/dist/migrate-mantle-state-Dkk5zGHw.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
1
|
import { C as ConfigError, S as validateConfig, _ as StateConfig, a as ConfigEnvironmentUniverseId, b as UniverseOverlayWithoutId, c as DisplayNamePrefixConfig, d as GistStateConfig, f as PlaceEntry, g as ResourceEntryByKind, h as ResolvedUniverseEntry, i as Config, l as EnvironmentEntry, m as ResolvedPlaceEntry, n as ConfigInput, o as ConfigRootUniverseId, p as ResolvedConfig, r as defineConfig, s as DeveloperProductEntry, t as ConfigContext, u as GamePassEntry, v as UniverseEntry, w as ConfigValidationIssue, x as isGistStateConfig, y as UniverseOverlayWithId } from "./define-config-87u2jqjM.mjs";
|
|
2
|
-
import { Type as Type$1 } from "arktype";
|
|
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,708 @@ 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
|
-
/**
|
|
1234
|
-
* Minimal `fetch`-compatible signature the adapter needs, narrower than
|
|
1235
|
-
* `typeof globalThis.fetch` so test fakes do not have to stub runtime
|
|
1236
|
-
* extensions such as `fetch.preconnect`.
|
|
1237
|
-
*/
|
|
1238
|
-
type GistFetch = (input: globalThis.Request | string | URL, init?: RequestInit) => Promise<Response>;
|
|
1239
1215
|
/**
|
|
1240
|
-
*
|
|
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.
|
|
1241
1221
|
*/
|
|
1242
|
-
interface
|
|
1243
|
-
/**
|
|
1244
|
-
readonly
|
|
1245
|
-
/**
|
|
1246
|
-
readonly
|
|
1247
|
-
/**
|
|
1248
|
-
|
|
1249
|
-
* promise. Tests pass a fake to keep retry assertions deterministic.
|
|
1250
|
-
*/
|
|
1251
|
-
readonly sleep?: ((ms: number) => Promise<void>) | undefined;
|
|
1252
|
-
/** GitHub token (fine-grained PAT or classic PAT) with gist read/write scope. */
|
|
1253
|
-
readonly token: string;
|
|
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";
|
|
1254
1229
|
}
|
|
1230
|
+
/** Failure modes returned by {@link selectEnvironment}. */
|
|
1231
|
+
type SelectEnvironmentError = IncompletePlaceEntryError | IncompleteUniverseEntryError | UnknownEnvironmentError;
|
|
1255
1232
|
/**
|
|
1256
|
-
*
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
1233
|
+
* Project a validated `Config` onto a single environment. Looks up the
|
|
1234
|
+
* matching `environments[environment]` entry, deep-merges its resource
|
|
1235
|
+
* overlay (`passes`, `places`, `universe`) over the root config via defu,
|
|
1236
|
+
* and applies the env-level state override when present (the env entry's
|
|
1237
|
+
* `state` field wins; otherwise the root `state` flows through).
|
|
1261
1238
|
*
|
|
1262
|
-
*
|
|
1239
|
+
* Pure: no I/O. Returns a `ResolvedConfig` ready to feed into downstream
|
|
1240
|
+
* functions (`flattenConfig`, `buildDefaultRegistry`, `resolveStateConfig`).
|
|
1241
|
+
* The post-merge view promotes `places` from `Record<string, PlaceEntry>`
|
|
1242
|
+
* (root: file-paths only) to `Record<string, ResolvedPlaceEntry>` (root +
|
|
1243
|
+
* overlay merged). `environments` and `extends` are passed through
|
|
1244
|
+
* unchanged because they preserve the shape relationship to `Config`;
|
|
1245
|
+
* downstream consumers do not read them post-merge.
|
|
1263
1246
|
*
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1247
|
+
* Defu's merge semantics are deliberate: keyed-map collections merge by
|
|
1248
|
+
* key (so a place declared in both root and overlay produces a single
|
|
1249
|
+
* entry whose overlay-supplied fields win), and `null` / `undefined` in
|
|
1250
|
+
* the overlay are skipped (so the overlay never deletes a root field).
|
|
1251
|
+
* State has its own resolution path (a single replacement, not a
|
|
1252
|
+
* deep-merge) because it is a tagged union: a deep-merge of
|
|
1253
|
+
* `{ backend: "s3" }` over `{ backend: "gist", gistId }` would produce
|
|
1254
|
+
* a malformed `{ backend: "s3", gistId }`.
|
|
1266
1255
|
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
* token: "ghp_example",
|
|
1272
|
-
* });
|
|
1256
|
+
* State is left absent when neither the env override nor the root block
|
|
1257
|
+
* provides one. Callers that require a resolved `StateConfig` should
|
|
1258
|
+
* route through `resolveStateConfig` or `buildStatePort`; the absent
|
|
1259
|
+
* case surfaces as a typed `stateNotConfigured` there.
|
|
1273
1260
|
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1261
|
+
* Limitation in v1: a per-environment universe overlay that introduces a
|
|
1262
|
+
* brand-new universe block may still have optional fields missing, since
|
|
1263
|
+
* the overlay type only requires the identity-bearing key. The resolver
|
|
1264
|
+
* surfaces the entry as-is; the universe driver reports the missing
|
|
1265
|
+
* field when it tries to consume the entry. Universe is a singleton with
|
|
1266
|
+
* 20+ optional fields, so the same `incompletePlaceEntry`-style validation
|
|
1267
|
+
* is deferred to a separate follow-up.
|
|
1281
1268
|
*
|
|
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.
|
|
1269
|
+
* When the project sets a `displayNamePrefix` (or omits it, in which case
|
|
1270
|
+
* prefixing defaults to enabled) and the chosen environment declares a
|
|
1271
|
+
* non-empty `label`, the resolver renders the configured template via
|
|
1272
|
+
* `renderDisplayNamePrefix` and prepends the result to `universe.displayName`
|
|
1273
|
+
* and every declared place `displayName`. An undeclared `displayName`, an
|
|
1274
|
+
* empty/absent label, or an explicit `displayNamePrefix.enabled: false` all
|
|
1275
|
+
* skip prefixing for the affected fields.
|
|
1294
1276
|
*
|
|
1295
1277
|
* @example
|
|
1296
1278
|
*
|
|
1297
1279
|
* ```ts
|
|
1298
|
-
* import
|
|
1299
|
-
* import {
|
|
1300
|
-
* import { asRobloxAssetId, type PlaceDriverDeps } from "@bedrock-rbx/core";
|
|
1280
|
+
* import { selectEnvironment } from "@bedrock-rbx/core";
|
|
1281
|
+
* import type { Config } from "@bedrock-rbx/core/config";
|
|
1301
1282
|
*
|
|
1302
|
-
* const
|
|
1303
|
-
*
|
|
1304
|
-
*
|
|
1283
|
+
* const config: Config = {
|
|
1284
|
+
* environments: {
|
|
1285
|
+
* production: { universe: { universeId: "999" } },
|
|
1305
1286
|
* },
|
|
1287
|
+
* state: { backend: "gist", gistId: "abc123" },
|
|
1288
|
+
* universe: { voiceChatEnabled: true },
|
|
1306
1289
|
* };
|
|
1307
1290
|
*
|
|
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
|
-
* };
|
|
1291
|
+
* const result = selectEnvironment(config, "production");
|
|
1317
1292
|
*
|
|
1318
|
-
* expect(
|
|
1293
|
+
* expect(result.success).toBeTrue();
|
|
1294
|
+
* if (result.success) {
|
|
1295
|
+
* expect(result.data.universe?.universeId).toBe("999");
|
|
1296
|
+
* expect(result.data.universe?.voiceChatEnabled).toBeTrue();
|
|
1297
|
+
* expect(result.data.state?.backend).toBe("gist");
|
|
1298
|
+
* }
|
|
1319
1299
|
* ```
|
|
1300
|
+
*
|
|
1301
|
+
* @param config - Validated project config carrying at least one
|
|
1302
|
+
* environment under `environments`.
|
|
1303
|
+
* @param environment - Environment name to project onto. Must be a key
|
|
1304
|
+
* of `config.environments`.
|
|
1305
|
+
* @returns `Ok(ResolvedConfig)` with the merged resource fields and the
|
|
1306
|
+
* resolved state, or `Err(SelectEnvironmentError)` describing why the
|
|
1307
|
+
* projection failed.
|
|
1320
1308
|
*/
|
|
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
|
-
}
|
|
1309
|
+
declare function selectEnvironment(config: Config, environment: string): Result$1<ResolvedConfig, SelectEnvironmentError>;
|
|
1310
|
+
//#endregion
|
|
1311
|
+
//#region src/ports/resource-driver.d.ts
|
|
1329
1312
|
/**
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
* upstream
|
|
1333
|
-
* place" endpoint (the place is user-supplied input), only "publish version".
|
|
1313
|
+
* Plugin contract for a resource adapter: the interface a third-party author
|
|
1314
|
+
* implements to teach Bedrock how to reconcile one {@link ResourceKind} against
|
|
1315
|
+
* its upstream API.
|
|
1334
1316
|
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1317
|
+
* `ResourceDriver<K>` is a *driven* (secondary) port in hexagonal terms; the
|
|
1318
|
+
* name "driver" follows Terraform, Pulumi, and Mantle IaC convention for a
|
|
1319
|
+
* component that talks to a specific resource API.
|
|
1338
1320
|
*
|
|
1339
|
-
* @
|
|
1340
|
-
* @returns A driver indexable by `"place"` in a `DriverRegistry`.
|
|
1341
|
-
* @throws Whatever `deps.readFile` rejects with.
|
|
1321
|
+
* @template K - The {@link ResourceKind} discriminator this driver handles.
|
|
1342
1322
|
*
|
|
1343
1323
|
* @example
|
|
1344
1324
|
*
|
|
1345
1325
|
* ```ts
|
|
1346
|
-
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
1347
|
-
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
1348
1326
|
* import {
|
|
1349
1327
|
* asResourceKey,
|
|
1350
1328
|
* asRobloxAssetId,
|
|
1351
1329
|
* asSha256Hex,
|
|
1352
|
-
*
|
|
1330
|
+
* type ResourceDriver,
|
|
1353
1331
|
* } from "@bedrock-rbx/core";
|
|
1354
1332
|
*
|
|
1355
|
-
* const
|
|
1356
|
-
* async
|
|
1357
|
-
* return {
|
|
1358
|
-
* data: { body: { versionNumber: 1 }, headers: {}, status: 200 },
|
|
1359
|
-
* success: true,
|
|
1360
|
-
* };
|
|
1361
|
-
* },
|
|
1362
|
-
* };
|
|
1363
|
-
*
|
|
1364
|
-
* const driver = createPlaceDriver({
|
|
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
|
|
1379
|
-
* .create({
|
|
1380
|
-
* description: undefined,
|
|
1381
|
-
* displayName: undefined,
|
|
1382
|
-
* fileHash: asSha256Hex(
|
|
1383
|
-
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1384
|
-
* ),
|
|
1385
|
-
* filePath: "places/start.rbxl",
|
|
1386
|
-
* key: asResourceKey("start-place"),
|
|
1387
|
-
* kind: "place",
|
|
1388
|
-
* placeId: asRobloxAssetId("4711"),
|
|
1389
|
-
* serverSize: undefined,
|
|
1390
|
-
* })
|
|
1391
|
-
* .then((result) => {
|
|
1392
|
-
* expect(result.success).toBeTrue();
|
|
1393
|
-
* if (result.success) {
|
|
1394
|
-
* expect(result.data.outputs.versionNumber).toBe(1);
|
|
1395
|
-
* }
|
|
1396
|
-
* });
|
|
1397
|
-
* ```
|
|
1398
|
-
*/
|
|
1399
|
-
declare function createPlaceDriver(deps: PlaceDriverDeps): ResourceDriver<"place">;
|
|
1400
|
-
//#endregion
|
|
1401
|
-
//#region src/adapters/universe-driver.d.ts
|
|
1402
|
-
/**
|
|
1403
|
-
* Dependencies of `createUniverseDriver`. The driver reconciles the
|
|
1404
|
-
* universe singleton against both the universes endpoint and the root
|
|
1405
|
-
* place (for fields Roblox marks read-only on the universe, like
|
|
1406
|
-
* `displayName`). There is no `universeId` at construction time because
|
|
1407
|
-
* the universe *is* the resource the driver reconciles, so the ID rides
|
|
1408
|
-
* along on each `UniverseDesiredState`.
|
|
1409
|
-
*/
|
|
1410
|
-
interface UniverseDriverDeps {
|
|
1411
|
-
/** Configured places client from `@bedrock-rbx/ocale/places`. */
|
|
1412
|
-
readonly places: PlacesClient;
|
|
1413
|
-
/** Configured universes client from `@bedrock-rbx/ocale/universes`. */
|
|
1414
|
-
readonly universes: UniversesClient;
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* Wraps {@link UniversesClient} as a `ResourceDriver<"universe">`. `create`
|
|
1418
|
-
* and `update` both delegate to a shared reconcile helper because Open
|
|
1419
|
-
* Cloud cannot mint universes; the user supplies an existing `universeId`
|
|
1420
|
-
* and bedrock adopts the universe on first apply.
|
|
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`.
|
|
1440
|
-
*
|
|
1441
|
-
* @example
|
|
1442
|
-
*
|
|
1443
|
-
* ```ts
|
|
1444
|
-
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
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";
|
|
1453
|
-
*
|
|
1454
|
-
* const universeBodyHttpClient: HttpClient = {
|
|
1455
|
-
* async request() {
|
|
1333
|
+
* const gamePassDriver: ResourceDriver<"gamePass"> = {
|
|
1334
|
+
* async create(desired) {
|
|
1456
1335
|
* return {
|
|
1457
1336
|
* data: {
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
* status: 200,
|
|
1337
|
+
* ...desired,
|
|
1338
|
+
* outputs: {
|
|
1339
|
+
* assetId: asRobloxAssetId("9876543210"),
|
|
1340
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
|
|
1341
|
+
* },
|
|
1464
1342
|
* },
|
|
1465
1343
|
* success: true,
|
|
1466
1344
|
* };
|
|
1467
1345
|
* },
|
|
1468
1346
|
* };
|
|
1469
1347
|
*
|
|
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
|
|
1348
|
+
* return gamePassDriver
|
|
1484
1349
|
* .create({
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1350
|
+
* description: "Grants VIP perks.",
|
|
1351
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
1352
|
+
* iconFileHashes: {
|
|
1353
|
+
* "en-us": asSha256Hex(
|
|
1354
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1355
|
+
* ),
|
|
1356
|
+
* },
|
|
1357
|
+
* key: asResourceKey("vip-pass"),
|
|
1358
|
+
* kind: "gamePass",
|
|
1359
|
+
* name: "VIP Pass",
|
|
1360
|
+
* price: undefined,
|
|
1496
1361
|
* })
|
|
1497
1362
|
* .then((result) => {
|
|
1498
1363
|
* expect(result.success).toBeTrue();
|
|
1499
1364
|
* if (result.success) {
|
|
1500
|
-
* expect(result.data.outputs.
|
|
1365
|
+
* expect(result.data.outputs.assetId).toBe("9876543210");
|
|
1501
1366
|
* }
|
|
1502
1367
|
* });
|
|
1503
1368
|
* ```
|
|
1504
1369
|
*/
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1370
|
+
interface ResourceDriver<K extends ResourceKind> {
|
|
1371
|
+
/**
|
|
1372
|
+
* Create the resource upstream from its desired state and return the
|
|
1373
|
+
* resulting current state (desired fields + Roblox-assigned outputs).
|
|
1374
|
+
*/
|
|
1375
|
+
create(desired: Extract<ResourceDesiredState, {
|
|
1376
|
+
kind: K;
|
|
1377
|
+
}>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
|
|
1378
|
+
/**
|
|
1379
|
+
* Reconcile an upstream resource whose managed content has drifted from its
|
|
1380
|
+
* desired state. Receives the last-known current state so the driver can
|
|
1381
|
+
* compute a minimal patch (or no-op upstream, for file-backed kinds where
|
|
1382
|
+
* republishing is unconditional).
|
|
1383
|
+
*
|
|
1384
|
+
* Optional. Drivers whose upstream API has no update operation omit this
|
|
1385
|
+
* method; `applyOps` surfaces an `updateUnsupported` error at dispatch time
|
|
1386
|
+
* instead.
|
|
1387
|
+
*/
|
|
1388
|
+
update?(current: ResourceCurrentState<K>, desired: Extract<ResourceDesiredState, {
|
|
1389
|
+
kind: K;
|
|
1390
|
+
}>): Promise<Result$1<ResourceCurrentState<K>, OpenCloudError$1>>;
|
|
1519
1391
|
}
|
|
1520
1392
|
/**
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
* `desired.
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
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.
|
|
1393
|
+
* Polymorphic dispatch table keyed by {@link ResourceKind}, mapping each kind
|
|
1394
|
+
* to the {@link ResourceDriver} that handles it. `applyOps` indexes the
|
|
1395
|
+
* registry by `op.desired.kind` to reach the matching driver with full type
|
|
1396
|
+
* safety: adding a new kind to `ResourceDesiredState` is a compile error until
|
|
1397
|
+
* a matching registry entry is supplied.
|
|
1531
1398
|
*
|
|
1532
1399
|
* @example
|
|
1533
1400
|
*
|
|
1534
1401
|
* ```ts
|
|
1535
|
-
* import {
|
|
1402
|
+
* import { OpenCloudError, type DriverRegistry } from "@bedrock-rbx/core";
|
|
1536
1403
|
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1404
|
+
* const registry: DriverRegistry = {
|
|
1405
|
+
* gamePass: {
|
|
1406
|
+
* async create() {
|
|
1407
|
+
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
1408
|
+
* },
|
|
1409
|
+
* },
|
|
1410
|
+
* place: {
|
|
1411
|
+
* async create() {
|
|
1412
|
+
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
1413
|
+
* },
|
|
1414
|
+
* },
|
|
1415
|
+
* universe: {
|
|
1416
|
+
* async create() {
|
|
1417
|
+
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
1418
|
+
* },
|
|
1419
|
+
* },
|
|
1420
|
+
* developerProduct: {
|
|
1421
|
+
* async create() {
|
|
1422
|
+
* return { err: new OpenCloudError("not implemented"), success: false };
|
|
1423
|
+
* },
|
|
1424
|
+
* },
|
|
1425
|
+
* };
|
|
1426
|
+
*
|
|
1427
|
+
* expect(registry.gamePass).toBeObject();
|
|
1539
1428
|
* ```
|
|
1540
1429
|
*/
|
|
1541
|
-
|
|
1542
|
-
readonly price: number | undefined;
|
|
1543
|
-
}): PriceFields;
|
|
1430
|
+
type DriverRegistry = { [K in ResourceKind]: ResourceDriver<K> };
|
|
1544
1431
|
//#endregion
|
|
1545
1432
|
//#region src/core/operations.d.ts
|
|
1546
1433
|
/**
|
|
@@ -1738,162 +1625,228 @@ interface NoopOperation extends BaseOperation {
|
|
|
1738
1625
|
*/
|
|
1739
1626
|
type Operation = CreateOperation | NoopOperation | UpdateOperation;
|
|
1740
1627
|
//#endregion
|
|
1741
|
-
//#region src/
|
|
1628
|
+
//#region src/shell/apply-ops.d.ts
|
|
1742
1629
|
/**
|
|
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.
|
|
1630
|
+
* Failure surfaced by `applyOps` when an operation cannot be applied.
|
|
1631
|
+
* Plain-data discriminated union; narrow on `kind`, do not `instanceof` it.
|
|
1755
1632
|
*
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
1758
|
-
*
|
|
1759
|
-
*
|
|
1633
|
+
* `appliedSoFar` carries the driver outputs from operations that succeeded
|
|
1634
|
+
* before the failing one, in dispatched order. Callers persist this so a
|
|
1635
|
+
* follow-up reconcile does not duplicate Roblox-side resources that have
|
|
1636
|
+
* already been created or updated.
|
|
1760
1637
|
*
|
|
1761
1638
|
* @example
|
|
1762
1639
|
*
|
|
1763
1640
|
* ```ts
|
|
1764
|
-
* import {
|
|
1765
|
-
* asResourceKey,
|
|
1766
|
-
* asRobloxAssetId,
|
|
1767
|
-
* asSha256Hex,
|
|
1768
|
-
* diff,
|
|
1769
|
-
* type GamePassDesiredState,
|
|
1770
|
-
* type ResourceCurrentState,
|
|
1771
|
-
* } from "@bedrock-rbx/core";
|
|
1641
|
+
* import { asResourceKey, type ApplyError } from "@bedrock-rbx/core";
|
|
1772
1642
|
*
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1643
|
+
* function describe(err: ApplyError): string {
|
|
1644
|
+
* switch (err.kind) {
|
|
1645
|
+
* case "driverFailure": {
|
|
1646
|
+
* return `driver failed for ${err.key}: ${err.cause.message}`;
|
|
1647
|
+
* }
|
|
1648
|
+
* case "updateUnsupported": {
|
|
1649
|
+
* return `update not supported for ${err.key}`;
|
|
1650
|
+
* }
|
|
1651
|
+
* }
|
|
1652
|
+
* }
|
|
1776
1653
|
*
|
|
1777
|
-
* const
|
|
1778
|
-
* description: "Grants VIP perks.",
|
|
1779
|
-
* icon: { "en-us": "assets/vip-icon.png" },
|
|
1780
|
-
* iconFileHashes: { "en-us": hash },
|
|
1654
|
+
* const err: ApplyError = {
|
|
1781
1655
|
* 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",
|
|
1656
|
+
* appliedSoFar: [],
|
|
1657
|
+
* kind: "updateUnsupported",
|
|
1795
1658
|
* };
|
|
1796
1659
|
*
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1660
|
+
* expect(describe(err)).toBe("update not supported for vip-pass");
|
|
1661
|
+
* ```
|
|
1662
|
+
*/
|
|
1663
|
+
type ApplyError = {
|
|
1664
|
+
readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
|
|
1665
|
+
readonly cause: OpenCloudError$1;
|
|
1666
|
+
readonly key: ResourceKey;
|
|
1667
|
+
readonly kind: "driverFailure";
|
|
1668
|
+
} | {
|
|
1669
|
+
readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
|
|
1670
|
+
readonly key: ResourceKey;
|
|
1671
|
+
readonly kind: "updateUnsupported";
|
|
1672
|
+
};
|
|
1673
|
+
/**
|
|
1674
|
+
* Dispatch each reconciliation operation to the matching resource driver
|
|
1675
|
+
* with first-fail semantics: on the first `Err` (driver failure or
|
|
1676
|
+
* `updateUnsupported`), the remaining operations are skipped and the error
|
|
1677
|
+
* is returned verbatim.
|
|
1678
|
+
*
|
|
1679
|
+
* Behaviour:
|
|
1680
|
+
* - `create` operations are routed to `registry[op.desired.kind].create`.
|
|
1681
|
+
* - `update` operations are routed to `registry[op.desired.kind].update`
|
|
1682
|
+
* when the driver exposes it; otherwise they short-circuit to an
|
|
1683
|
+
* `updateUnsupported` Err without invoking the driver.
|
|
1684
|
+
* - `noop` operations are skipped entirely (no I/O, no dispatch).
|
|
1685
|
+
*
|
|
1686
|
+
* On success the returned array carries the driver outputs for every
|
|
1687
|
+
* non-noop op, in dispatched order. Noops are not represented; callers
|
|
1688
|
+
* needing a full post-apply snapshot merge with the pre-apply current
|
|
1689
|
+
* state keyed by `ResourceKey`.
|
|
1690
|
+
*
|
|
1691
|
+
* @param ops - Reconciliation operations produced by `diff`, applied in order.
|
|
1692
|
+
* @param registry - Per-kind driver table; dispatch uses `op.desired.kind` as the index.
|
|
1693
|
+
* @returns `Ok(state)` when every operation succeeds, where `state` holds
|
|
1694
|
+
* driver outputs for each non-noop op in dispatched order; or the first
|
|
1695
|
+
* failure encountered.
|
|
1696
|
+
* @throws Whatever the dispatched driver rejects with outside its `Result`
|
|
1697
|
+
* return. A driver whose injected I/O (file reads, network calls, etc.)
|
|
1698
|
+
* throws will surface that rejection here rather than translating it into
|
|
1699
|
+
* a `Result` failure; wrap the call site in a try/catch when drivers are
|
|
1700
|
+
* not trusted to contain their own rejections.
|
|
1701
|
+
* @example
|
|
1702
|
+
*
|
|
1703
|
+
* ```ts
|
|
1704
|
+
* import {
|
|
1705
|
+
* applyOps,
|
|
1706
|
+
* asResourceKey,
|
|
1707
|
+
* asRobloxAssetId,
|
|
1708
|
+
* asSha256Hex,
|
|
1709
|
+
* type DriverRegistry,
|
|
1710
|
+
* type Operation,
|
|
1711
|
+
* } from "@bedrock-rbx/core";
|
|
1712
|
+
*
|
|
1713
|
+
* const registry: DriverRegistry = {
|
|
1714
|
+
* gamePass: {
|
|
1715
|
+
* async create(desired) {
|
|
1716
|
+
* return {
|
|
1717
|
+
* data: {
|
|
1718
|
+
* ...desired,
|
|
1719
|
+
* outputs: {
|
|
1720
|
+
* assetId: asRobloxAssetId("9876543210"),
|
|
1721
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("1122334455") },
|
|
1722
|
+
* },
|
|
1723
|
+
* },
|
|
1724
|
+
* success: true,
|
|
1725
|
+
* };
|
|
1726
|
+
* },
|
|
1727
|
+
* },
|
|
1728
|
+
* place: {
|
|
1729
|
+
* async create(desired) {
|
|
1730
|
+
* return {
|
|
1731
|
+
* data: { ...desired, outputs: { versionNumber: 1 } },
|
|
1732
|
+
* success: true,
|
|
1733
|
+
* };
|
|
1734
|
+
* },
|
|
1735
|
+
* },
|
|
1736
|
+
* universe: {
|
|
1737
|
+
* async create(desired) {
|
|
1738
|
+
* return {
|
|
1739
|
+
* data: { ...desired, outputs: { rootPlaceId: asRobloxAssetId("4711") } },
|
|
1740
|
+
* success: true,
|
|
1741
|
+
* };
|
|
1742
|
+
* },
|
|
1743
|
+
* },
|
|
1744
|
+
* developerProduct: {
|
|
1745
|
+
* async create(desired) {
|
|
1746
|
+
* return {
|
|
1747
|
+
* data: {
|
|
1748
|
+
* ...desired,
|
|
1749
|
+
* outputs: { productId: asRobloxAssetId("8172635495") },
|
|
1750
|
+
* },
|
|
1751
|
+
* success: true,
|
|
1752
|
+
* };
|
|
1803
1753
|
* },
|
|
1804
1754
|
* },
|
|
1755
|
+
* };
|
|
1756
|
+
*
|
|
1757
|
+
* const ops: ReadonlyArray<Operation> = [
|
|
1805
1758
|
* {
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1759
|
+
* key: asResourceKey("vip-pass"),
|
|
1760
|
+
* type: "create",
|
|
1761
|
+
* desired: {
|
|
1762
|
+
* key: asResourceKey("vip-pass"),
|
|
1763
|
+
* name: "VIP Pass",
|
|
1764
|
+
* description: "Grants VIP perks.",
|
|
1765
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
1766
|
+
* iconFileHashes: {
|
|
1767
|
+
* "en-us": asSha256Hex(
|
|
1768
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
1769
|
+
* ),
|
|
1770
|
+
* },
|
|
1771
|
+
* kind: "gamePass",
|
|
1772
|
+
* price: 500,
|
|
1811
1773
|
* },
|
|
1812
1774
|
* },
|
|
1813
1775
|
* ];
|
|
1814
1776
|
*
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1817
|
-
*
|
|
1777
|
+
* return applyOps(ops, registry).then((result) => {
|
|
1778
|
+
* expect(result.success).toBe(true);
|
|
1779
|
+
* expect(result.success && result.data).toHaveLength(1);
|
|
1780
|
+
* });
|
|
1818
1781
|
* ```
|
|
1819
1782
|
*/
|
|
1820
|
-
declare function
|
|
1783
|
+
declare function applyOps(ops: ReadonlyArray<Operation>, registry: DriverRegistry): Promise<Result$1<ReadonlyArray<ResourceCurrentState>, ApplyError>>;
|
|
1821
1784
|
//#endregion
|
|
1822
|
-
//#region src/
|
|
1785
|
+
//#region src/shell/build-default-registry.d.ts
|
|
1823
1786
|
/**
|
|
1824
|
-
*
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1787
|
+
* Failure surfaced when default-constructing a registry needs a config
|
|
1788
|
+
* field that is not present. The deploy boundary wraps this in a
|
|
1789
|
+
* `DeployError` so the caller sees a typed Result instead of a downstream
|
|
1790
|
+
* driver error.
|
|
1827
1791
|
*/
|
|
1828
|
-
|
|
1792
|
+
interface RegistryConfigError {
|
|
1793
|
+
/** Suggested fix routed back to the caller. */
|
|
1794
|
+
readonly hint: string;
|
|
1795
|
+
/** Literal discriminator for narrowing. */
|
|
1796
|
+
readonly kind: "registryConfigMissing";
|
|
1797
|
+
/** Which config field was missing. */
|
|
1798
|
+
readonly missing: "universeId";
|
|
1799
|
+
}
|
|
1800
|
+
/** Inputs for {@link buildDefaultRegistry}. */
|
|
1801
|
+
interface BuildDefaultRegistryDeps {
|
|
1802
|
+
/** Resolved project config; supplies `universe.universeId` and is read for nothing else. */
|
|
1803
|
+
readonly config: ResolvedConfig;
|
|
1804
|
+
/** Reads an environment variable; injected so tests stay free of `process.env`. */
|
|
1805
|
+
readonly getEnv: (name: string) => string | undefined;
|
|
1806
|
+
/** Reader plumbed into kind-specific drivers that ingest file bytes. */
|
|
1807
|
+
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
1808
|
+
}
|
|
1829
1809
|
/**
|
|
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.
|
|
1810
|
+
* Construct the default `DriverRegistry` from `config.universe.universeId`
|
|
1811
|
+
* and `BEDROCK_API_KEY`. Reads the API key via the injected `getEnv` seam
|
|
1812
|
+
* and surfaces `missingCredential` or `registryConfigMissing` as typed
|
|
1813
|
+
* Results instead of throwing.
|
|
1844
1814
|
*
|
|
1845
1815
|
* @example
|
|
1846
1816
|
*
|
|
1847
1817
|
* ```ts
|
|
1848
|
-
* import {
|
|
1818
|
+
* import { buildDefaultRegistry } from "@bedrock-rbx/core";
|
|
1849
1819
|
*
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1820
|
+
* const registry = buildDefaultRegistry({
|
|
1821
|
+
* config: {
|
|
1822
|
+
* environments: { production: {} },
|
|
1823
|
+
* state: { backend: "gist", gistId: "abc" },
|
|
1824
|
+
* universe: { universeId: "1234567890" },
|
|
1825
|
+
* },
|
|
1826
|
+
* getEnv: () => "rbx-test",
|
|
1827
|
+
* readFile: async () => new Uint8Array(),
|
|
1828
|
+
* });
|
|
1829
|
+
*
|
|
1830
|
+
* expect(registry.success).toBeTrue();
|
|
1853
1831
|
* ```
|
|
1832
|
+
*
|
|
1833
|
+
* @param deps - Validated config plus credential and file-reader seams.
|
|
1834
|
+
* @returns A `DriverRegistry` on success, or a typed Err describing the
|
|
1835
|
+
* missing API key or the missing universe declaration.
|
|
1854
1836
|
*/
|
|
1855
|
-
declare function
|
|
1837
|
+
declare function buildDefaultRegistry(deps: BuildDefaultRegistryDeps): Result$1<DriverRegistry, MissingCredentialError | RegistryConfigError>;
|
|
1856
1838
|
//#endregion
|
|
1857
|
-
//#region src/core/
|
|
1839
|
+
//#region src/core/flatten.d.ts
|
|
1858
1840
|
/**
|
|
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.
|
|
1841
|
+
* Pre-I/O game-pass input the flattener emits. Extends the authored
|
|
1842
|
+
* `GamePassEntry` with the tag discriminator and the `ResourceKey`-branded
|
|
1843
|
+
* key so `buildDesired` can consume a flat tagged list and layer on the
|
|
1844
|
+
* SHA-256 icon digest.
|
|
1867
1845
|
*
|
|
1868
1846
|
* @example
|
|
1869
1847
|
*
|
|
1870
1848
|
* ```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";
|
|
1849
|
+
* import { asResourceKey, type GamePassDesiredInput } from "@bedrock-rbx/core";
|
|
1897
1850
|
*
|
|
1898
1851
|
* const input: GamePassDesiredInput = {
|
|
1899
1852
|
* description: "Grants VIP perks.",
|
|
@@ -2110,47 +2063,6 @@ type ResourceDesiredInput = DeveloperProductDesiredInput | GamePassDesiredInput
|
|
|
2110
2063
|
*/
|
|
2111
2064
|
declare function flattenConfig(config: ResolvedConfig): ReadonlyArray<ResourceDesiredInput>;
|
|
2112
2065
|
//#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
2066
|
//#region src/core/kinds/module.d.ts
|
|
2155
2067
|
/**
|
|
2156
2068
|
* Failure surfaced during desired-state preparation. Two variants today:
|
|
@@ -2325,1007 +2237,1284 @@ type InputFor<K extends ResourceKind> = Extract<ResourceDesiredInput, {
|
|
|
2325
2237
|
readonly kind: K;
|
|
2326
2238
|
}>;
|
|
2327
2239
|
//#endregion
|
|
2328
|
-
//#region src/
|
|
2240
|
+
//#region src/shell/build-desired.d.ts
|
|
2329
2241
|
/**
|
|
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.
|
|
2242
|
+
* Layer file I/O onto a flat tagged list of resource inputs to produce
|
|
2243
|
+
* `ResourceDesiredState`.
|
|
2340
2244
|
*
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
2245
|
+
* For each input, reads the file bytes via the injected `readFile`, computes
|
|
2246
|
+
* the SHA-256 hex digest, and assembles the branded desired-state record
|
|
2247
|
+
* that `diff` consumes. Entries are processed sequentially so the first
|
|
2248
|
+
* failure's attribution is deterministic.
|
|
2345
2249
|
*
|
|
2250
|
+
* @param inputs - Flat tagged resource inputs from `flattenConfig`.
|
|
2251
|
+
* @param readFile - Reads file bytes for a given path; rejection becomes a
|
|
2252
|
+
* `fileReadFailed` Err.
|
|
2253
|
+
* @returns `Ok` with the desired-state array (same length and order as
|
|
2254
|
+
* `inputs`), or `Err` with the first I/O failure.
|
|
2346
2255
|
* @example
|
|
2347
2256
|
*
|
|
2348
2257
|
* ```ts
|
|
2349
|
-
* import {
|
|
2258
|
+
* import { asResourceKey, buildDesired } from "@bedrock-rbx/core";
|
|
2350
2259
|
*
|
|
2351
|
-
*
|
|
2352
|
-
*
|
|
2353
|
-
*
|
|
2354
|
-
* const fresh = asSha256Hex(
|
|
2355
|
-
* "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
|
|
2356
|
-
* );
|
|
2260
|
+
* async function readFile(): Promise<Uint8Array> {
|
|
2261
|
+
* return new Uint8Array([1, 2, 3]);
|
|
2262
|
+
* }
|
|
2357
2263
|
*
|
|
2358
|
-
*
|
|
2359
|
-
*
|
|
2264
|
+
* return buildDesired(
|
|
2265
|
+
* [
|
|
2266
|
+
* {
|
|
2267
|
+
* description: "Grants VIP perks.",
|
|
2268
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
2269
|
+
* key: asResourceKey("vip-pass"),
|
|
2270
|
+
* kind: "gamePass",
|
|
2271
|
+
* name: "VIP Pass",
|
|
2272
|
+
* price: 500,
|
|
2273
|
+
* },
|
|
2274
|
+
* ],
|
|
2275
|
+
* readFile,
|
|
2276
|
+
* ).then((result) => {
|
|
2277
|
+
* expect(result.success).toBeTrue();
|
|
2278
|
+
* if (result.success) {
|
|
2279
|
+
* expect(result.data).toHaveLength(1);
|
|
2280
|
+
* expect(result.data[0]!.kind).toBe("gamePass");
|
|
2281
|
+
* }
|
|
2282
|
+
* });
|
|
2360
2283
|
* ```
|
|
2361
2284
|
*/
|
|
2362
|
-
declare function
|
|
2285
|
+
declare function buildDesired(inputs: ReadonlyArray<ResourceDesiredInput>, readFile: (path: string) => Promise<Uint8Array>): Promise<Result$1<ReadonlyArray<ResourceDesiredState>, BuildDesiredError>>;
|
|
2363
2286
|
//#endregion
|
|
2364
|
-
//#region src/
|
|
2287
|
+
//#region src/shell/load-config.d.ts
|
|
2365
2288
|
/**
|
|
2366
|
-
*
|
|
2367
|
-
*
|
|
2368
|
-
|
|
2369
|
-
|
|
2289
|
+
* Options for {@link loadConfig}. Matches a subset of c12's loader options;
|
|
2290
|
+
* additional fields land with the issues that introduce each flow.
|
|
2291
|
+
*/
|
|
2292
|
+
interface LoadConfigOptions {
|
|
2293
|
+
/**
|
|
2294
|
+
* Path to a specific config file to load, including its extension.
|
|
2295
|
+
* Resolved relative to `cwd` when not absolute. Loaded as-is with no
|
|
2296
|
+
* extension search; if the file does not exist at the given path,
|
|
2297
|
+
* `loadConfig` returns `fileNotFound`. When omitted, `loadConfig`
|
|
2298
|
+
* discovers `bedrock.config.{ts,js,...}` from `cwd`.
|
|
2299
|
+
*/
|
|
2300
|
+
readonly configFile?: string;
|
|
2301
|
+
/**
|
|
2302
|
+
* Directory to search from. Defaults to `process.cwd()` at call time, so
|
|
2303
|
+
* each invocation sees the current working directory.
|
|
2304
|
+
*/
|
|
2305
|
+
readonly cwd?: string;
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Discover, parse, and validate the project config.
|
|
2309
|
+
*
|
|
2310
|
+
* Looks for `bedrock.config.{ts,js,mjs,cjs,yaml,yml,json}`, `.bedrockrc*`,
|
|
2311
|
+
* and `package.json#bedrock` starting at `options.cwd` (or the current
|
|
2312
|
+
* working directory). Returns a fresh, mutable `Config` on every call so
|
|
2313
|
+
* long-running scripts see up-to-date values.
|
|
2314
|
+
*
|
|
2315
|
+
* When the exported default is a function (sync or async), `loadConfig`
|
|
2316
|
+
* invokes it with an empty `ConfigContext` and awaits the result before
|
|
2317
|
+
* validating.
|
|
2318
|
+
*
|
|
2319
|
+
* Errors return via `Result`:
|
|
2320
|
+
* - `fileNotFound` - no config file was discovered under the search path.
|
|
2321
|
+
* - `parseFailed` - a config file was found but could not be parsed (for
|
|
2322
|
+
* example, malformed YAML or JSON).
|
|
2323
|
+
* - `validationFailed` - a file was found and parsed, but its content did
|
|
2324
|
+
* not satisfy the runtime schema.
|
|
2325
|
+
* - `configFunctionFailed` - a function-form config threw or its returned
|
|
2326
|
+
* promise rejected while being invoked.
|
|
2370
2327
|
*
|
|
2328
|
+
* @param options - Loader options.
|
|
2329
|
+
* @returns `Ok` with the validated `Config`, or `Err` with a `ConfigError`.
|
|
2371
2330
|
* @example
|
|
2372
2331
|
*
|
|
2373
2332
|
* ```ts
|
|
2374
|
-
* import {
|
|
2333
|
+
* import { loadConfig } from "@bedrock-rbx/core";
|
|
2375
2334
|
*
|
|
2376
|
-
*
|
|
2377
|
-
*
|
|
2378
|
-
*
|
|
2379
|
-
*
|
|
2335
|
+
* return loadConfig({
|
|
2336
|
+
* configFile: "bedrock.staging.config.yaml",
|
|
2337
|
+
* cwd: "/path/that/does/not/have/a/config",
|
|
2338
|
+
* }).then((result) => {
|
|
2339
|
+
* expect(result.success).toBeFalse();
|
|
2340
|
+
* if (!result.success) {
|
|
2341
|
+
* expect(result.err.kind).toBe("fileNotFound");
|
|
2342
|
+
* }
|
|
2343
|
+
* });
|
|
2380
2344
|
* ```
|
|
2381
2345
|
*/
|
|
2382
|
-
declare
|
|
2346
|
+
declare function loadConfig(options?: LoadConfigOptions): Promise<Result$1<Config, ConfigError>>;
|
|
2383
2347
|
//#endregion
|
|
2384
|
-
//#region src/
|
|
2348
|
+
//#region src/shell/deploy.d.ts
|
|
2385
2349
|
/**
|
|
2386
|
-
*
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
2389
|
-
* `JSON.stringify` for downstream logging and parallel-iterates cleanly
|
|
2390
|
-
* with `Config.environments` (which is itself a `Record`).
|
|
2350
|
+
* Inputs for `deploy`. Every field except `environment` is optional;
|
|
2351
|
+
* omitted dependencies are default-constructed from the project config
|
|
2352
|
+
* and the environment variables `GITHUB_TOKEN` and `BEDROCK_API_KEY`.
|
|
2391
2353
|
*/
|
|
2392
|
-
|
|
2354
|
+
interface DeployOptions {
|
|
2355
|
+
/** Pre-loaded, optionally-mutated project config. Omit to call `loadConfig()` automatically. */
|
|
2356
|
+
readonly config?: Config;
|
|
2357
|
+
/** Environment name; threaded into `StatePort.read` and the persisted snapshot. */
|
|
2358
|
+
readonly environment: string;
|
|
2359
|
+
/** `fetch` override plumbed into the default-constructed gist adapter when `statePort` is omitted. */
|
|
2360
|
+
readonly fetch?: GistFetch;
|
|
2361
|
+
/** Reads an environment variable; defaults to `(name) => process.env[name]`. */
|
|
2362
|
+
readonly getEnv?: (name: string) => string | undefined;
|
|
2363
|
+
/** Loader invoked when `config` is omitted; defaults to `loadConfig` from this package. */
|
|
2364
|
+
readonly loadConfig?: (options?: LoadConfigOptions) => Promise<Result$1<Config, ConfigError>>;
|
|
2365
|
+
/** Reads file bytes for resources that have file-backed inputs. Defaults to `node:fs/promises.readFile`. */
|
|
2366
|
+
readonly readFile?: (path: string) => Promise<Uint8Array>;
|
|
2367
|
+
/** Per-kind driver table consulted for create / update dispatch. Default-constructed from `BEDROCK_API_KEY` when omitted. */
|
|
2368
|
+
readonly registry?: DriverRegistry;
|
|
2369
|
+
/** Backend used to read the prior snapshot and persist the new one. Default-constructed from `config.state` and `GITHUB_TOKEN` when omitted. */
|
|
2370
|
+
readonly statePort?: StatePort;
|
|
2371
|
+
}
|
|
2393
2372
|
/**
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
2396
|
-
*
|
|
2373
|
+
* Failure surfaced by `deploy`. Stage-tagged so callers can branch on
|
|
2374
|
+
* `kind` to distinguish reconciliation failures (`stateReadFailed`,
|
|
2375
|
+
* `applyFailed`, ...) from default-construction failures
|
|
2376
|
+
* (`configLoadFailed`, `stateNotConfigured`, `unknownEnvironment`,
|
|
2377
|
+
* `incompletePlaceEntry`, `incompleteUniverseEntry`, `missingCredential`,
|
|
2378
|
+
* `unsupportedBackend`, `registryConfigMissing`).
|
|
2397
2379
|
*/
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
readonly
|
|
2401
|
-
|
|
2402
|
-
readonly
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
readonly
|
|
2407
|
-
}
|
|
2380
|
+
type DeployError = IncompletePlaceEntryError | IncompleteUniverseEntryError | MissingCredentialError | RegistryConfigError | StateNotConfiguredError | UnknownEnvironmentError | UnsupportedBackendError | {
|
|
2381
|
+
readonly cause: ApplyError;
|
|
2382
|
+
readonly kind: "applyFailed";
|
|
2383
|
+
} | {
|
|
2384
|
+
readonly cause: BuildDesiredError;
|
|
2385
|
+
readonly kind: "buildDesiredFailed";
|
|
2386
|
+
} | {
|
|
2387
|
+
readonly cause: ConfigError;
|
|
2388
|
+
readonly kind: "configLoadFailed";
|
|
2389
|
+
} | {
|
|
2390
|
+
readonly cause: StateError;
|
|
2391
|
+
readonly kind: "stateReadFailed";
|
|
2392
|
+
} | {
|
|
2393
|
+
readonly cause: StateError;
|
|
2394
|
+
readonly kind: "stateWriteFailed";
|
|
2395
|
+
readonly unsavedState: BedrockState;
|
|
2396
|
+
};
|
|
2408
2397
|
/**
|
|
2409
|
-
*
|
|
2410
|
-
*
|
|
2398
|
+
* Run a full reconcile end-to-end. Default-constructs missing deps from
|
|
2399
|
+
* the project config and the environment variables `GITHUB_TOKEN` and
|
|
2400
|
+
* `BEDROCK_API_KEY`; never reads `process.env` when `statePort`,
|
|
2401
|
+
* `registry`, and `config` are all supplied explicitly.
|
|
2411
2402
|
*
|
|
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.
|
|
2403
|
+
* @param options - Target environment plus optional overrides.
|
|
2404
|
+
* @returns The persisted `BedrockState` on success, or a stage-tagged
|
|
2405
|
+
* `DeployError` on failure.
|
|
2406
|
+
* @example
|
|
2422
2407
|
*
|
|
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`.
|
|
2408
|
+
* ```ts
|
|
2409
|
+
* import { deploy } from "@bedrock-rbx/core";
|
|
2448
2410
|
*
|
|
2449
|
-
*
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
2452
|
-
*
|
|
2453
|
-
*
|
|
2454
|
-
*
|
|
2455
|
-
*
|
|
2456
|
-
*
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
2459
|
-
*
|
|
2460
|
-
*
|
|
2461
|
-
*
|
|
2462
|
-
*
|
|
2463
|
-
*
|
|
2464
|
-
*
|
|
2465
|
-
*
|
|
2411
|
+
* return deploy({ environment: "production" }).then((result) => {
|
|
2412
|
+
* expect(result.success).toBeFalse();
|
|
2413
|
+
* if (!result.success) {
|
|
2414
|
+
* expect(["configLoadFailed", "stateNotConfigured"]).toContain(result.err.kind);
|
|
2415
|
+
* }
|
|
2416
|
+
* });
|
|
2417
|
+
* ```
|
|
2418
|
+
*
|
|
2419
|
+
* @example
|
|
2420
|
+
*
|
|
2421
|
+
* ```ts
|
|
2422
|
+
* import { deploy, type BedrockState, type DriverRegistry, type StatePort } from "@bedrock-rbx/core";
|
|
2423
|
+
*
|
|
2424
|
+
* const store = new Map<string, BedrockState>();
|
|
2425
|
+
* const statePort: StatePort = {
|
|
2426
|
+
* async read(environment) {
|
|
2427
|
+
* return { data: store.get(environment), success: true };
|
|
2428
|
+
* },
|
|
2429
|
+
* async write(state) {
|
|
2430
|
+
* store.set(state.environment, state);
|
|
2431
|
+
* return { data: undefined, success: true };
|
|
2432
|
+
* },
|
|
2433
|
+
* };
|
|
2434
|
+
* const registry: DriverRegistry = {
|
|
2435
|
+
* developerProduct: {
|
|
2436
|
+
* create: async () => { throw new Error("unreachable: empty config"); },
|
|
2437
|
+
* },
|
|
2438
|
+
* gamePass: { create: async () => { throw new Error("unreachable: empty config"); } },
|
|
2439
|
+
* place: { create: async () => { throw new Error("unreachable: empty config"); } },
|
|
2440
|
+
* universe: { create: async () => { throw new Error("unreachable: empty config"); } },
|
|
2441
|
+
* };
|
|
2442
|
+
*
|
|
2443
|
+
* return deploy({
|
|
2444
|
+
* config: {
|
|
2445
|
+
* environments: { production: {} },
|
|
2446
|
+
* state: { backend: "gist", gistId: "abc" },
|
|
2447
|
+
* passes: {},
|
|
2448
|
+
* },
|
|
2449
|
+
* environment: "production",
|
|
2450
|
+
* registry,
|
|
2451
|
+
* statePort,
|
|
2452
|
+
* }).then((result) => {
|
|
2453
|
+
* expect(result.success).toBeTrue();
|
|
2454
|
+
* if (result.success) {
|
|
2455
|
+
* expect(result.data.environment).toBe("production");
|
|
2456
|
+
* expect(result.data.resources).toBeEmpty();
|
|
2457
|
+
* }
|
|
2458
|
+
* });
|
|
2459
|
+
* ```
|
|
2466
2460
|
*/
|
|
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
|
-
};
|
|
2461
|
+
declare function deploy(options: DeployOptions): Promise<Result$1<BedrockState, DeployError>>;
|
|
2462
|
+
//#endregion
|
|
2463
|
+
//#region src/cli/render.d.ts
|
|
2490
2464
|
/**
|
|
2491
|
-
*
|
|
2465
|
+
* Output port the CLI renders through. Mirrors the subset of `@clack/prompts`
|
|
2466
|
+
* the bedrock CLI uses today; tests inject a fake to assert what was rendered.
|
|
2492
2467
|
*
|
|
2493
|
-
*
|
|
2494
|
-
* already validated against the runtime schema (a failure to validate
|
|
2495
|
-
* surfaces as `MigrateError.internalError`, not as a returned report).
|
|
2468
|
+
* @example
|
|
2496
2469
|
*
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
*
|
|
2470
|
+
* ```ts
|
|
2471
|
+
* import type { ClackPort } from "@bedrock-rbx/core";
|
|
2472
|
+
*
|
|
2473
|
+
* const lines: Array<string> = [];
|
|
2474
|
+
* const port: ClackPort = {
|
|
2475
|
+
* cancel: (message) => lines.push(`cancel: ${message}`),
|
|
2476
|
+
* intro: (message) => lines.push(`intro: ${message}`),
|
|
2477
|
+
* logError: (message) => lines.push(`error: ${message}`),
|
|
2478
|
+
* logMessage: (message) => lines.push(`log: ${message}`),
|
|
2479
|
+
* logSuccess: (message) => lines.push(`ok: ${message}`),
|
|
2480
|
+
* outro: (message) => lines.push(`outro: ${message}`),
|
|
2481
|
+
* };
|
|
2500
2482
|
*
|
|
2501
|
-
*
|
|
2502
|
-
* environment from the input. Truthful per environment (no factorization)
|
|
2503
|
-
* so `bedrock deploy --env=<env>` produces zero ops on first run.
|
|
2483
|
+
* port.logSuccess("done");
|
|
2504
2484
|
*
|
|
2505
|
-
*
|
|
2506
|
-
*
|
|
2485
|
+
* expect(lines).toEqual(["ok: done"]);
|
|
2486
|
+
* ```
|
|
2507
2487
|
*/
|
|
2508
|
-
interface
|
|
2509
|
-
/**
|
|
2510
|
-
|
|
2511
|
-
/**
|
|
2512
|
-
|
|
2513
|
-
/**
|
|
2514
|
-
|
|
2515
|
-
/**
|
|
2516
|
-
|
|
2517
|
-
/**
|
|
2518
|
-
|
|
2488
|
+
interface ClackPort {
|
|
2489
|
+
/** End an interactive flow with a cancellation marker. */
|
|
2490
|
+
cancel(message: string): void;
|
|
2491
|
+
/** Open a framed section with a title (used for command intros). */
|
|
2492
|
+
intro(message: string): void;
|
|
2493
|
+
/** Render a single error line inside an open frame. */
|
|
2494
|
+
logError(message: string): void;
|
|
2495
|
+
/** Render a single neutral line inside an open frame. */
|
|
2496
|
+
logMessage(message: string): void;
|
|
2497
|
+
/** Render a single success line inside an open frame. */
|
|
2498
|
+
logSuccess(message: string): void;
|
|
2499
|
+
/** Close the current framed section with a final message. */
|
|
2500
|
+
outro(message: string): void;
|
|
2519
2501
|
}
|
|
2520
2502
|
//#endregion
|
|
2521
|
-
//#region src/
|
|
2503
|
+
//#region src/ports/progress-port.d.ts
|
|
2522
2504
|
/**
|
|
2523
|
-
*
|
|
2524
|
-
*
|
|
2525
|
-
*
|
|
2526
|
-
* where state should live.
|
|
2505
|
+
* Per-environment outcome event emitted after a deploy completes
|
|
2506
|
+
* successfully. Carries the environment name and the count of resources
|
|
2507
|
+
* present in the persisted state snapshot.
|
|
2527
2508
|
*/
|
|
2528
|
-
interface
|
|
2529
|
-
/**
|
|
2509
|
+
interface DeploySuccessEvent {
|
|
2510
|
+
/** The environment that finished reconciling. */
|
|
2530
2511
|
readonly environment: string;
|
|
2531
|
-
/**
|
|
2532
|
-
readonly kind: "
|
|
2512
|
+
/** Discriminator tag. */
|
|
2513
|
+
readonly kind: "deploySuccess";
|
|
2514
|
+
/** Number of resources in the post-deploy state snapshot. */
|
|
2515
|
+
readonly resourceCount: number;
|
|
2533
2516
|
}
|
|
2534
2517
|
/**
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
* the resolver to the discriminated-union arms.
|
|
2518
|
+
* Per-environment outcome event emitted when a deploy fails. Carries the
|
|
2519
|
+
* environment name and the full {@link DeployError} so a renderer can
|
|
2520
|
+
* delegate to the existing diagnostic helpers.
|
|
2539
2521
|
*/
|
|
2540
|
-
interface
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
readonly
|
|
2522
|
+
interface DeployFailureEvent {
|
|
2523
|
+
/** The environment whose deploy failed. */
|
|
2524
|
+
readonly environment: string;
|
|
2525
|
+
/** Stage-tagged failure reason returned by the shell `deploy` function. */
|
|
2526
|
+
readonly error: DeployError;
|
|
2527
|
+
/** Discriminator tag. */
|
|
2528
|
+
readonly kind: "deployFailure";
|
|
2545
2529
|
}
|
|
2546
2530
|
/**
|
|
2547
|
-
*
|
|
2548
|
-
*
|
|
2549
|
-
*
|
|
2550
|
-
|
|
2531
|
+
* Discriminated union of progress events the CLI emits while a deploy
|
|
2532
|
+
* runs. The variant set is additive: future per-stage and per-resource
|
|
2533
|
+
* events land as new `kind` values without breaking existing adapters.
|
|
2534
|
+
*/
|
|
2535
|
+
type ProgressEvent = DeployFailureEvent | DeploySuccessEvent;
|
|
2536
|
+
/**
|
|
2537
|
+
* Plugin contract for receiving deploy outcomes: the interface an adapter
|
|
2538
|
+
* (clack renderer, JSON logger, custom UI) implements to let the CLI hand
|
|
2539
|
+
* off events without re-implementing rendering logic.
|
|
2540
|
+
*
|
|
2541
|
+
* `ProgressPort` is a *driven* (secondary) port in hexagonal terms.
|
|
2551
2542
|
*
|
|
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
2543
|
* @example
|
|
2557
2544
|
*
|
|
2558
2545
|
* ```ts
|
|
2559
|
-
* import {
|
|
2546
|
+
* import type { ProgressEvent, ProgressPort } from "@bedrock-rbx/core";
|
|
2560
2547
|
*
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
2565
|
-
* production: { state: { backend: "gist", gistId: "prod-gist" } },
|
|
2566
|
-
* },
|
|
2548
|
+
* let received: ReadonlyArray<ProgressEvent> = [];
|
|
2549
|
+
* const port: ProgressPort = {
|
|
2550
|
+
* emit(event) {
|
|
2551
|
+
* received = [...received, event];
|
|
2567
2552
|
* },
|
|
2568
|
-
*
|
|
2569
|
-
* );
|
|
2553
|
+
* };
|
|
2570
2554
|
*
|
|
2571
|
-
*
|
|
2572
|
-
*
|
|
2573
|
-
*
|
|
2574
|
-
* }
|
|
2555
|
+
* port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 });
|
|
2556
|
+
*
|
|
2557
|
+
* expect(received).toEqual([
|
|
2558
|
+
* { environment: "production", kind: "deploySuccess", resourceCount: 3 },
|
|
2559
|
+
* ]);
|
|
2575
2560
|
* ```
|
|
2576
2561
|
*/
|
|
2577
|
-
|
|
2562
|
+
interface ProgressPort {
|
|
2563
|
+
/** Hand a single progress event to the adapter for rendering or logging. */
|
|
2564
|
+
emit(event: ProgressEvent): void;
|
|
2565
|
+
}
|
|
2578
2566
|
//#endregion
|
|
2579
|
-
//#region src/
|
|
2567
|
+
//#region src/adapters/clack-progress-adapter.d.ts
|
|
2580
2568
|
/**
|
|
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.
|
|
2569
|
+
* Configuration for {@link createClackProgressAdapter}.
|
|
2585
2570
|
*/
|
|
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";
|
|
2571
|
+
interface ClackProgressAdapterDeps {
|
|
2572
|
+
/** Output port the events are rendered through. */
|
|
2573
|
+
readonly clack: ClackPort;
|
|
2593
2574
|
}
|
|
2594
2575
|
/**
|
|
2595
|
-
*
|
|
2596
|
-
*
|
|
2597
|
-
*
|
|
2598
|
-
*
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2576
|
+
* Build a {@link ProgressPort} that renders events through a `ClackPort`.
|
|
2577
|
+
* Pattern-matches on the event `kind`: `deploySuccess` becomes a single
|
|
2578
|
+
* success line and `deployFailure` delegates to the package's deploy-error
|
|
2579
|
+
* rendering helper.
|
|
2580
|
+
*
|
|
2581
|
+
* @example
|
|
2582
|
+
*
|
|
2583
|
+
* ```ts
|
|
2584
|
+
* import { createClackProgressAdapter, type ClackPort } from "@bedrock-rbx/core";
|
|
2585
|
+
*
|
|
2586
|
+
* const lines: Array<string> = [];
|
|
2587
|
+
* const clack: ClackPort = {
|
|
2588
|
+
* cancel: (message) => lines.push(`cancel: ${message}`),
|
|
2589
|
+
* intro: (message) => lines.push(`intro: ${message}`),
|
|
2590
|
+
* logError: (message) => lines.push(`error: ${message}`),
|
|
2591
|
+
* logMessage: (message) => lines.push(`log: ${message}`),
|
|
2592
|
+
* logSuccess: (message) => lines.push(`ok: ${message}`),
|
|
2593
|
+
* outro: (message) => lines.push(`outro: ${message}`),
|
|
2594
|
+
* };
|
|
2595
|
+
*
|
|
2596
|
+
* const port = createClackProgressAdapter({ clack });
|
|
2597
|
+
*
|
|
2598
|
+
* port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 });
|
|
2599
|
+
*
|
|
2600
|
+
* expect(lines).toEqual(["ok: production: 3 resources reconciled"]);
|
|
2601
|
+
* ```
|
|
2602
|
+
*
|
|
2603
|
+
* @param deps - The clack port the adapter renders through.
|
|
2604
|
+
* @returns A `ProgressPort` that renders via clack.
|
|
2602
2605
|
*/
|
|
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
|
-
}
|
|
2606
|
+
declare function createClackProgressAdapter(deps: ClackProgressAdapterDeps): ProgressPort;
|
|
2607
|
+
//#endregion
|
|
2608
|
+
//#region src/adapters/developer-product-driver.d.ts
|
|
2613
2609
|
/**
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
*
|
|
2617
|
-
*
|
|
2618
|
-
*
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2610
|
+
* Dependencies of `createDeveloperProductDriver`. `universeId` is captured
|
|
2611
|
+
* at construction time (matching `GamePassDriverDeps`) so each driver
|
|
2612
|
+
* instance is bound to a single universe; multi-universe deploys construct
|
|
2613
|
+
* one driver per universe. `readFile` exists on the driver (not upstream
|
|
2614
|
+
* in shell) because icon hashes flow through `diff` but bytes do not.
|
|
2615
|
+
*
|
|
2616
|
+
* @example
|
|
2617
|
+
*
|
|
2618
|
+
* ```ts
|
|
2619
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2620
|
+
* import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
|
|
2621
|
+
* import { asRobloxAssetId, type DeveloperProductDriverDeps } from "@bedrock-rbx/core";
|
|
2622
|
+
*
|
|
2623
|
+
* const httpClient: HttpClient = {
|
|
2624
|
+
* async request() {
|
|
2625
|
+
* return { data: { body: {}, headers: {}, status: 200 }, success: true };
|
|
2626
|
+
* },
|
|
2627
|
+
* };
|
|
2628
|
+
*
|
|
2629
|
+
* const deps: DeveloperProductDriverDeps = {
|
|
2630
|
+
* client: new DeveloperProductsClient({
|
|
2631
|
+
* apiKey: "rbx-your-key",
|
|
2632
|
+
* httpClient,
|
|
2633
|
+
* sleep: async () => {},
|
|
2634
|
+
* }),
|
|
2635
|
+
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
2636
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2637
|
+
* };
|
|
2638
|
+
*
|
|
2639
|
+
* expect(deps.universeId).toBe("1234567890");
|
|
2640
|
+
* ```
|
|
2641
|
+
*/
|
|
2642
|
+
interface DeveloperProductDriverDeps {
|
|
2643
|
+
/** Configured developer-products client from `@bedrock-rbx/ocale/developer-products`. */
|
|
2644
|
+
readonly client: DeveloperProductsClient;
|
|
2645
|
+
/** Reads icon bytes for upload; rejections propagate out of `create` and `update`. */
|
|
2646
|
+
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
2647
|
+
/** Universe that owns every developer product this driver creates. */
|
|
2648
|
+
readonly universeId: RobloxAssetId;
|
|
2627
2649
|
}
|
|
2628
|
-
/** Failure modes returned by {@link selectEnvironment}. */
|
|
2629
|
-
type SelectEnvironmentError = IncompletePlaceEntryError | IncompleteUniverseEntryError | UnknownEnvironmentError;
|
|
2630
2650
|
/**
|
|
2631
|
-
*
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
2651
|
+
* Wraps {@link DeveloperProductsClient} as a `ResourceDriver<"developerProduct">`
|
|
2652
|
+
* that maps a desired-state entry to an ocale create or update call and the
|
|
2653
|
+
* response back to a `ResourceCurrentState<"developerProduct">`. The
|
|
2654
|
+
* `update` path consumes the upstream `204 No Content` response and
|
|
2655
|
+
* synthesizes the post-update `ResourceCurrentState` from `desired` plus
|
|
2656
|
+
* the existing `current.outputs`, carrying `iconImageAssetId` forward when
|
|
2657
|
+
* present.
|
|
2636
2658
|
*
|
|
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.
|
|
2659
|
+
* Upstream `OpenCloudError` results pass through as `Result` failures.
|
|
2644
2660
|
*
|
|
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 }`.
|
|
2661
|
+
* @param deps - Injected ocale client and owning universe.
|
|
2662
|
+
* @returns A driver indexable by `"developerProduct"` in a `DriverRegistry`.
|
|
2653
2663
|
*
|
|
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.
|
|
2664
|
+
* @example
|
|
2658
2665
|
*
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
2661
|
-
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2665
|
-
*
|
|
2666
|
+
* ```ts
|
|
2667
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2668
|
+
* import { DeveloperProductsClient } from "@bedrock-rbx/ocale/developer-products";
|
|
2669
|
+
* import {
|
|
2670
|
+
* asResourceKey,
|
|
2671
|
+
* asRobloxAssetId,
|
|
2672
|
+
* createDeveloperProductDriver,
|
|
2673
|
+
* } from "@bedrock-rbx/core";
|
|
2674
|
+
*
|
|
2675
|
+
* const httpClient: HttpClient = {
|
|
2676
|
+
* async request() {
|
|
2677
|
+
* return {
|
|
2678
|
+
* data: {
|
|
2679
|
+
* body: {
|
|
2680
|
+
* createdTimestamp: "2024-01-15T10:30:00.000Z",
|
|
2681
|
+
* description: "Stocks the player up with 1,000 premium gems.",
|
|
2682
|
+
* iconImageAssetId: null,
|
|
2683
|
+
* isForSale: false,
|
|
2684
|
+
* isImmutable: false,
|
|
2685
|
+
* name: "Gem Pack",
|
|
2686
|
+
* priceInformation: null,
|
|
2687
|
+
* productId: 9_876_543_210,
|
|
2688
|
+
* storePageEnabled: false,
|
|
2689
|
+
* universeId: 1_234_567_890,
|
|
2690
|
+
* updatedTimestamp: "2024-01-15T10:30:00.000Z",
|
|
2691
|
+
* },
|
|
2692
|
+
* headers: {},
|
|
2693
|
+
* status: 200,
|
|
2694
|
+
* },
|
|
2695
|
+
* success: true,
|
|
2696
|
+
* };
|
|
2697
|
+
* },
|
|
2698
|
+
* };
|
|
2699
|
+
*
|
|
2700
|
+
* const driver = createDeveloperProductDriver({
|
|
2701
|
+
* client: new DeveloperProductsClient({
|
|
2702
|
+
* apiKey: "rbx-your-key",
|
|
2703
|
+
* httpClient,
|
|
2704
|
+
* sleep: async () => {},
|
|
2705
|
+
* }),
|
|
2706
|
+
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
2707
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2708
|
+
* });
|
|
2709
|
+
*
|
|
2710
|
+
* return driver
|
|
2711
|
+
* .create({
|
|
2712
|
+
* description: "Stocks the player up with 1,000 premium gems.",
|
|
2713
|
+
* isRegionalPricingEnabled: undefined,
|
|
2714
|
+
* key: asResourceKey("gem-pack"),
|
|
2715
|
+
* kind: "developerProduct",
|
|
2716
|
+
* name: "Gem Pack",
|
|
2717
|
+
* price: undefined,
|
|
2718
|
+
* storePageEnabled: undefined,
|
|
2719
|
+
* })
|
|
2720
|
+
* .then((result) => {
|
|
2721
|
+
* expect(result.success).toBeTrue();
|
|
2722
|
+
* if (result.success) {
|
|
2723
|
+
* expect(result.data.outputs.productId).toBe("9876543210");
|
|
2724
|
+
* }
|
|
2725
|
+
* });
|
|
2726
|
+
* ```
|
|
2727
|
+
*/
|
|
2728
|
+
declare function createDeveloperProductDriver(deps: DeveloperProductDriverDeps): ResourceDriver<"developerProduct">;
|
|
2729
|
+
//#endregion
|
|
2730
|
+
//#region src/adapters/game-pass-driver.d.ts
|
|
2731
|
+
/**
|
|
2732
|
+
* `universeId` is captured at construction time rather than on
|
|
2733
|
+
* `GamePassDesiredState` so state files round-trip with Mantle's `PassInputs`
|
|
2734
|
+
* shape. `readFile` exists on the driver (not upstream in shell) because icon
|
|
2735
|
+
* hashes flow through `diff` but bytes do not.
|
|
2736
|
+
*
|
|
2737
|
+
* @example
|
|
2738
|
+
*
|
|
2739
|
+
* ```ts
|
|
2740
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2741
|
+
* import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
|
|
2742
|
+
* import { asRobloxAssetId, type GamePassDriverDeps } from "@bedrock-rbx/core";
|
|
2743
|
+
*
|
|
2744
|
+
* const httpClient: HttpClient = {
|
|
2745
|
+
* async request() {
|
|
2746
|
+
* return { data: { body: {}, headers: {}, status: 200 }, success: true };
|
|
2747
|
+
* },
|
|
2748
|
+
* };
|
|
2749
|
+
*
|
|
2750
|
+
* const deps: GamePassDriverDeps = {
|
|
2751
|
+
* client: new GamePassesClient({
|
|
2752
|
+
* apiKey: "rbx-your-key",
|
|
2753
|
+
* httpClient,
|
|
2754
|
+
* sleep: async () => {},
|
|
2755
|
+
* }),
|
|
2756
|
+
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
2757
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2758
|
+
* };
|
|
2759
|
+
*
|
|
2760
|
+
* expect(deps.universeId).toBe("1234567890");
|
|
2761
|
+
* ```
|
|
2762
|
+
*/
|
|
2763
|
+
interface GamePassDriverDeps {
|
|
2764
|
+
/** Configured game-passes client from `@bedrock-rbx/ocale/game-passes`. */
|
|
2765
|
+
readonly client: GamePassesClient;
|
|
2766
|
+
/** Reads icon bytes for upload; rejections propagate out of `create`. */
|
|
2767
|
+
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
2768
|
+
/** Universe that owns every game pass this driver creates. */
|
|
2769
|
+
readonly universeId: RobloxAssetId;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Wraps {@link GamePassesClient} as a `ResourceDriver<"gamePass">` that maps
|
|
2773
|
+
* a desired-state entry to an ocale create call and the response back to a
|
|
2774
|
+
* `ResourceCurrentState<"gamePass">`.
|
|
2775
|
+
*
|
|
2776
|
+
* Upstream `OpenCloudError` results pass through as `Result` failures.
|
|
2777
|
+
* Filesystem errors from `deps.readFile` do not fit the `OpenCloudError`
|
|
2778
|
+
* shape and propagate as promise rejections; shell callers are expected to
|
|
2779
|
+
* translate them if a unified error surface is required.
|
|
2780
|
+
*
|
|
2781
|
+
* @param deps - Injected ocale client, file reader, and owning universe.
|
|
2782
|
+
* @returns A driver indexable by `"gamePass"` in a `DriverRegistry`.
|
|
2783
|
+
* @throws Whatever `deps.readFile` rejects with.
|
|
2784
|
+
*
|
|
2785
|
+
* @example
|
|
2786
|
+
*
|
|
2787
|
+
* ```ts
|
|
2788
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2789
|
+
* import { GamePassesClient } from "@bedrock-rbx/ocale/game-passes";
|
|
2790
|
+
* import {
|
|
2791
|
+
* asResourceKey,
|
|
2792
|
+
* asRobloxAssetId,
|
|
2793
|
+
* asSha256Hex,
|
|
2794
|
+
* createGamePassDriver,
|
|
2795
|
+
* } from "@bedrock-rbx/core";
|
|
2796
|
+
*
|
|
2797
|
+
* const httpClient: HttpClient = {
|
|
2798
|
+
* async request() {
|
|
2799
|
+
* return {
|
|
2800
|
+
* data: {
|
|
2801
|
+
* body: {
|
|
2802
|
+
* createdTimestamp: "2024-01-15T10:30:00.000Z",
|
|
2803
|
+
* description: "Grants VIP perks.",
|
|
2804
|
+
* gamePassId: 9_876_543_210,
|
|
2805
|
+
* iconAssetId: 1_122_334_455,
|
|
2806
|
+
* isForSale: true,
|
|
2807
|
+
* name: "VIP Pass",
|
|
2808
|
+
* updatedTimestamp: "2024-01-15T10:30:00.000Z",
|
|
2809
|
+
* },
|
|
2810
|
+
* headers: {},
|
|
2811
|
+
* status: 200,
|
|
2812
|
+
* },
|
|
2813
|
+
* success: true,
|
|
2814
|
+
* };
|
|
2815
|
+
* },
|
|
2816
|
+
* };
|
|
2817
|
+
*
|
|
2818
|
+
* const driver = createGamePassDriver({
|
|
2819
|
+
* client: new GamePassesClient({
|
|
2820
|
+
* apiKey: "rbx-your-key",
|
|
2821
|
+
* httpClient,
|
|
2822
|
+
* sleep: async () => {},
|
|
2823
|
+
* }),
|
|
2824
|
+
* readFile: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
|
|
2825
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2826
|
+
* });
|
|
2827
|
+
*
|
|
2828
|
+
* return driver
|
|
2829
|
+
* .create({
|
|
2830
|
+
* description: "Grants VIP perks.",
|
|
2831
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
2832
|
+
* iconFileHashes: {
|
|
2833
|
+
* "en-us": asSha256Hex(
|
|
2834
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
2835
|
+
* ),
|
|
2836
|
+
* },
|
|
2837
|
+
* key: asResourceKey("vip-pass"),
|
|
2838
|
+
* kind: "gamePass",
|
|
2839
|
+
* name: "VIP Pass",
|
|
2840
|
+
* price: 500,
|
|
2841
|
+
* })
|
|
2842
|
+
* .then((result) => {
|
|
2843
|
+
* expect(result.success).toBeTrue();
|
|
2844
|
+
* if (result.success) {
|
|
2845
|
+
* expect(result.data.outputs.assetId).toBe("9876543210");
|
|
2846
|
+
* }
|
|
2847
|
+
* });
|
|
2848
|
+
* ```
|
|
2849
|
+
*/
|
|
2850
|
+
declare function createGamePassDriver(deps: GamePassDriverDeps): ResourceDriver<"gamePass">;
|
|
2851
|
+
//#endregion
|
|
2852
|
+
//#region src/adapters/no-op-progress-adapter.d.ts
|
|
2853
|
+
/**
|
|
2854
|
+
* Build a {@link ProgressPort} that silently drops every event. Useful for
|
|
2855
|
+
* tests and programmatic callers who want to invoke deploy logic without
|
|
2856
|
+
* any rendering.
|
|
2857
|
+
*
|
|
2858
|
+
* @example
|
|
2859
|
+
*
|
|
2860
|
+
* ```ts
|
|
2861
|
+
* import { createNoOpProgressAdapter } from "@bedrock-rbx/core";
|
|
2666
2862
|
*
|
|
2667
|
-
*
|
|
2668
|
-
*
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
*
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2863
|
+
* const port = createNoOpProgressAdapter();
|
|
2864
|
+
*
|
|
2865
|
+
* expect(() =>
|
|
2866
|
+
* port.emit({ environment: "production", kind: "deploySuccess", resourceCount: 3 }),
|
|
2867
|
+
* ).not.toThrow();
|
|
2868
|
+
* ```
|
|
2869
|
+
*
|
|
2870
|
+
* @returns A `ProgressPort` whose `emit` method is a no-op.
|
|
2871
|
+
*/
|
|
2872
|
+
declare function createNoOpProgressAdapter(): ProgressPort;
|
|
2873
|
+
//#endregion
|
|
2874
|
+
//#region src/adapters/place-driver.d.ts
|
|
2875
|
+
/**
|
|
2876
|
+
* Dependencies of `createPlaceDriver`. `universeId` is captured at
|
|
2877
|
+
* construction time (matching `GamePassDriverDeps`) so each driver instance
|
|
2878
|
+
* is bound to a single universe; multi-universe deploys construct one driver
|
|
2879
|
+
* per universe. `readFile` is injected because `diff` operates on file hashes
|
|
2880
|
+
* while the driver is the only place that needs the raw bytes.
|
|
2674
2881
|
*
|
|
2675
2882
|
* @example
|
|
2676
2883
|
*
|
|
2677
2884
|
* ```ts
|
|
2678
|
-
* import {
|
|
2679
|
-
* import
|
|
2885
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2886
|
+
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
2887
|
+
* import { asRobloxAssetId, type PlaceDriverDeps } from "@bedrock-rbx/core";
|
|
2680
2888
|
*
|
|
2681
|
-
* const
|
|
2682
|
-
*
|
|
2683
|
-
*
|
|
2889
|
+
* const httpClient: HttpClient = {
|
|
2890
|
+
* async request() {
|
|
2891
|
+
* return { data: { body: {}, headers: {}, status: 200 }, success: true };
|
|
2684
2892
|
* },
|
|
2685
|
-
* state: { backend: "gist", gistId: "abc123" },
|
|
2686
|
-
* universe: { voiceChatEnabled: true },
|
|
2687
2893
|
* };
|
|
2688
2894
|
*
|
|
2689
|
-
* const
|
|
2895
|
+
* const deps: PlaceDriverDeps = {
|
|
2896
|
+
* client: new PlacesClient({
|
|
2897
|
+
* apiKey: "rbx-your-key",
|
|
2898
|
+
* httpClient,
|
|
2899
|
+
* sleep: async () => {},
|
|
2900
|
+
* }),
|
|
2901
|
+
* readFile: async () => new Uint8Array(),
|
|
2902
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2903
|
+
* };
|
|
2690
2904
|
*
|
|
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
|
-
* }
|
|
2905
|
+
* expect(deps.universeId).toBe("1234567890");
|
|
2697
2906
|
* ```
|
|
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
2907
|
*/
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2908
|
+
interface PlaceDriverDeps {
|
|
2909
|
+
/** Configured places client from `@bedrock-rbx/ocale/places`. */
|
|
2910
|
+
readonly client: PlacesClient;
|
|
2911
|
+
/** Reads place-file bytes for upload; rejections propagate out of the driver. */
|
|
2912
|
+
readonly readFile: (path: string) => Promise<Uint8Array>;
|
|
2913
|
+
/** Universe that owns every place this driver publishes. */
|
|
2914
|
+
readonly universeId: RobloxAssetId;
|
|
2915
|
+
}
|
|
2710
2916
|
/**
|
|
2711
|
-
*
|
|
2712
|
-
*
|
|
2917
|
+
* Wraps {@link PlacesClient} as a `ResourceDriver<"place">`. `create` and
|
|
2918
|
+
* `update` are both thin wrappers over a shared publish helper because the
|
|
2919
|
+
* upstream Open Cloud call is identical either way: there is no "create
|
|
2920
|
+
* place" endpoint (the place is user-supplied input), only "publish version".
|
|
2713
2921
|
*
|
|
2714
|
-
*
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2717
|
-
*
|
|
2922
|
+
* Format is detected from the file extension (`.rbxl` → binary,
|
|
2923
|
+
* `.rbxlx` → XML); any other extension returns an `ApiError`-backed failure
|
|
2924
|
+
* without hitting the network.
|
|
2925
|
+
*
|
|
2926
|
+
* @param deps - Injected ocale client, file reader, and owning universe.
|
|
2927
|
+
* @returns A driver indexable by `"place"` in a `DriverRegistry`.
|
|
2928
|
+
* @throws Whatever `deps.readFile` rejects with.
|
|
2718
2929
|
*
|
|
2719
2930
|
* @example
|
|
2720
2931
|
*
|
|
2721
2932
|
* ```ts
|
|
2722
|
-
* import {
|
|
2933
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
2934
|
+
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
2935
|
+
* import {
|
|
2936
|
+
* asResourceKey,
|
|
2937
|
+
* asRobloxAssetId,
|
|
2938
|
+
* asSha256Hex,
|
|
2939
|
+
* createPlaceDriver,
|
|
2940
|
+
* } from "@bedrock-rbx/core";
|
|
2723
2941
|
*
|
|
2724
|
-
* const
|
|
2725
|
-
*
|
|
2726
|
-
*
|
|
2727
|
-
*
|
|
2942
|
+
* const httpClient: HttpClient = {
|
|
2943
|
+
* async request() {
|
|
2944
|
+
* return {
|
|
2945
|
+
* data: { body: { versionNumber: 1 }, headers: {}, status: 200 },
|
|
2946
|
+
* success: true,
|
|
2947
|
+
* };
|
|
2948
|
+
* },
|
|
2728
2949
|
* };
|
|
2729
2950
|
*
|
|
2730
|
-
* const
|
|
2731
|
-
*
|
|
2732
|
-
*
|
|
2733
|
-
*
|
|
2734
|
-
*
|
|
2951
|
+
* const driver = createPlaceDriver({
|
|
2952
|
+
* client: new PlacesClient({
|
|
2953
|
+
* apiKey: "rbx-your-key",
|
|
2954
|
+
* httpClient,
|
|
2955
|
+
* sleep: async () => {},
|
|
2956
|
+
* }),
|
|
2957
|
+
* readFile: async () =>
|
|
2958
|
+
* new Uint8Array([
|
|
2959
|
+
* 0x3c, 0x72, 0x6f, 0x62, 0x6c, 0x6f, 0x78, 0x21, 0x89, 0xff, 0x0d, 0x0a, 0x1a,
|
|
2960
|
+
* 0x0a,
|
|
2961
|
+
* ]),
|
|
2962
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
2735
2963
|
* });
|
|
2736
|
-
* ```
|
|
2737
2964
|
*
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2965
|
+
* return driver
|
|
2966
|
+
* .create({
|
|
2967
|
+
* description: undefined,
|
|
2968
|
+
* displayName: undefined,
|
|
2969
|
+
* fileHash: asSha256Hex(
|
|
2970
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
2971
|
+
* ),
|
|
2972
|
+
* filePath: "places/start.rbxl",
|
|
2973
|
+
* key: asResourceKey("start-place"),
|
|
2974
|
+
* kind: "place",
|
|
2975
|
+
* placeId: asRobloxAssetId("4711"),
|
|
2976
|
+
* serverSize: undefined,
|
|
2977
|
+
* })
|
|
2978
|
+
* .then((result) => {
|
|
2979
|
+
* expect(result.success).toBeTrue();
|
|
2980
|
+
* if (result.success) {
|
|
2981
|
+
* expect(result.data.outputs.versionNumber).toBe(1);
|
|
2982
|
+
* }
|
|
2983
|
+
* });
|
|
2984
|
+
* ```
|
|
2740
2985
|
*/
|
|
2741
|
-
declare function
|
|
2986
|
+
declare function createPlaceDriver(deps: PlaceDriverDeps): ResourceDriver<"place">;
|
|
2987
|
+
//#endregion
|
|
2988
|
+
//#region src/adapters/universe-driver.d.ts
|
|
2742
2989
|
/**
|
|
2743
|
-
*
|
|
2990
|
+
* Dependencies of `createUniverseDriver`. The driver reconciles the
|
|
2991
|
+
* universe singleton against both the universes endpoint and the root
|
|
2992
|
+
* place (for fields Roblox marks read-only on the universe, like
|
|
2993
|
+
* `displayName`). There is no `universeId` at construction time because
|
|
2994
|
+
* the universe *is* the resource the driver reconciles, so the ID rides
|
|
2995
|
+
* along on each `UniverseDesiredState`.
|
|
2996
|
+
*/
|
|
2997
|
+
interface UniverseDriverDeps {
|
|
2998
|
+
/** Configured places client from `@bedrock-rbx/ocale/places`. */
|
|
2999
|
+
readonly places: PlacesClient;
|
|
3000
|
+
/** Configured universes client from `@bedrock-rbx/ocale/universes`. */
|
|
3001
|
+
readonly universes: UniversesClient;
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Wraps {@link UniversesClient} as a `ResourceDriver<"universe">`. `create`
|
|
3005
|
+
* and `update` both delegate to a shared reconcile helper because Open
|
|
3006
|
+
* Cloud cannot mint universes; the user supplies an existing `universeId`
|
|
3007
|
+
* and bedrock adopts the universe on first apply.
|
|
2744
3008
|
*
|
|
2745
|
-
* A
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
3009
|
+
* A `NotFound` error (HTTP 404) from `UniversesClient.update` is repackaged
|
|
3010
|
+
* as an adoption-error `ApiError` whose message names the config key and
|
|
3011
|
+
* the `universeId`, so operators can tell adoption failure apart from
|
|
3012
|
+
* transient upstream errors. A successful response whose `rootPlaceId` is
|
|
3013
|
+
* absent surfaces as an `ApiError` with status 200, mirroring the
|
|
3014
|
+
* malformed-response guard in `GamePassDriver`.
|
|
3015
|
+
*
|
|
3016
|
+
* When `displayName` is declared, the driver routes that field through
|
|
3017
|
+
* `PlacesClient.update` on the root place after the universe PATCH
|
|
3018
|
+
* succeeds. A subsequent places failure surfaces to the caller as the
|
|
3019
|
+
* driver's error result without rolling back the prior universe patch,
|
|
3020
|
+
* so callers observing a partial failure should reconcile by
|
|
3021
|
+
* reapplying rather than assuming the universe-level fields are
|
|
3022
|
+
* unchanged.
|
|
3023
|
+
*
|
|
3024
|
+
* @param deps - Injected ocale clients (universes plus places for the
|
|
3025
|
+
* read-only universe fields Roblox derives from the root place).
|
|
3026
|
+
* @returns A driver indexable by `"universe"` in a `DriverRegistry`.
|
|
2748
3027
|
*
|
|
2749
3028
|
* @example
|
|
2750
3029
|
*
|
|
2751
3030
|
* ```ts
|
|
2752
|
-
* import {
|
|
3031
|
+
* import type { HttpClient } from "@bedrock-rbx/ocale";
|
|
3032
|
+
* import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
3033
|
+
* import { UniversesClient } from "@bedrock-rbx/ocale/universes";
|
|
3034
|
+
* import { validUniverseBody } from "@bedrock-rbx/ocale/testing";
|
|
3035
|
+
* import {
|
|
3036
|
+
* asRobloxAssetId,
|
|
3037
|
+
* createUniverseDriver,
|
|
3038
|
+
* UNIVERSE_SINGLETON_KEY,
|
|
3039
|
+
* } from "@bedrock-rbx/core";
|
|
2753
3040
|
*
|
|
2754
|
-
* const
|
|
2755
|
-
*
|
|
2756
|
-
*
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
3041
|
+
* const universeBodyHttpClient: HttpClient = {
|
|
3042
|
+
* async request() {
|
|
3043
|
+
* return {
|
|
3044
|
+
* data: {
|
|
3045
|
+
* body: validUniverseBody({
|
|
3046
|
+
* path: "universes/1234567890",
|
|
3047
|
+
* rootPlace: "universes/1234567890/places/4711",
|
|
3048
|
+
* }),
|
|
3049
|
+
* headers: {},
|
|
3050
|
+
* status: 200,
|
|
3051
|
+
* },
|
|
3052
|
+
* success: true,
|
|
3053
|
+
* };
|
|
3054
|
+
* },
|
|
3055
|
+
* };
|
|
2760
3056
|
*
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
*
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
3057
|
+
* const driver = createUniverseDriver({
|
|
3058
|
+
* places: new PlacesClient({
|
|
3059
|
+
* apiKey: "rbx-your-key",
|
|
3060
|
+
* httpClient: universeBodyHttpClient,
|
|
3061
|
+
* sleep: async () => {},
|
|
3062
|
+
* }),
|
|
3063
|
+
* universes: new UniversesClient({
|
|
3064
|
+
* apiKey: "rbx-your-key",
|
|
3065
|
+
* httpClient: universeBodyHttpClient,
|
|
3066
|
+
* sleep: async () => {},
|
|
3067
|
+
* }),
|
|
3068
|
+
* });
|
|
3069
|
+
*
|
|
3070
|
+
* return driver
|
|
3071
|
+
* .create({
|
|
3072
|
+
* consoleEnabled: undefined,
|
|
3073
|
+
* desktopEnabled: true,
|
|
3074
|
+
* displayName: undefined,
|
|
3075
|
+
* key: UNIVERSE_SINGLETON_KEY,
|
|
3076
|
+
* kind: "universe",
|
|
3077
|
+
* mobileEnabled: undefined,
|
|
3078
|
+
* privateServerPriceRobux: undefined,
|
|
3079
|
+
* tabletEnabled: undefined,
|
|
3080
|
+
* universeId: asRobloxAssetId("1234567890"),
|
|
3081
|
+
* voiceChatEnabled: true,
|
|
3082
|
+
* vrEnabled: undefined,
|
|
3083
|
+
* })
|
|
3084
|
+
* .then((result) => {
|
|
3085
|
+
* expect(result.success).toBeTrue();
|
|
3086
|
+
* if (result.success) {
|
|
3087
|
+
* expect(result.data.outputs.rootPlaceId).toBe("4711");
|
|
3088
|
+
* }
|
|
3089
|
+
* });
|
|
3090
|
+
* ```
|
|
2767
3091
|
*/
|
|
2768
|
-
declare function
|
|
3092
|
+
declare function createUniverseDriver(deps: UniverseDriverDeps): ResourceDriver<"universe">;
|
|
2769
3093
|
//#endregion
|
|
2770
|
-
//#region src/
|
|
3094
|
+
//#region src/cli/clack-port.d.ts
|
|
2771
3095
|
/**
|
|
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.
|
|
3096
|
+
* Construct a `ClackPort` whose methods delegate to `@clack/prompts`. The
|
|
3097
|
+
* resulting port writes to `process.stdout` via clack's defaults. Kept in
|
|
3098
|
+
* its own module so consumers that never need the clack-backed rendering
|
|
3099
|
+
* (programmatic deploys, custom adapters) do not pull `@clack/prompts`
|
|
3100
|
+
* into their bundle.
|
|
2786
3101
|
*
|
|
2787
3102
|
* @example
|
|
2788
3103
|
*
|
|
2789
3104
|
* ```ts
|
|
2790
|
-
* import {
|
|
3105
|
+
* import { createClackPort } from "@bedrock-rbx/core";
|
|
2791
3106
|
*
|
|
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
|
-
* );
|
|
3107
|
+
* const port = createClackPort();
|
|
2823
3108
|
*
|
|
2824
|
-
* expect(
|
|
2825
|
-
* if (!result.success) {
|
|
2826
|
-
* expect(result.err.kind).toBe("iconRemovalRejected");
|
|
2827
|
-
* }
|
|
3109
|
+
* expect(typeof port.logSuccess).toBe("function");
|
|
2828
3110
|
* ```
|
|
3111
|
+
*
|
|
3112
|
+
* @returns A port whose six methods each invoke the matching clack helper.
|
|
2829
3113
|
*/
|
|
2830
|
-
declare function
|
|
3114
|
+
declare function createClackPort(): ClackPort;
|
|
2831
3115
|
//#endregion
|
|
2832
|
-
//#region src/
|
|
3116
|
+
//#region src/core/derive-price-fields.d.ts
|
|
2833
3117
|
/**
|
|
2834
|
-
*
|
|
2835
|
-
*
|
|
3118
|
+
* Wire-shape pricing fragment produced by {@link derivePriceFields}: the
|
|
3119
|
+
* `isForSale` flag and an optional numeric `price`. Mirrors the multipart
|
|
3120
|
+
* fields the Open Cloud `developer-products` create and update endpoints
|
|
3121
|
+
* accept for setting Robux pricing.
|
|
3122
|
+
*/
|
|
3123
|
+
interface PriceFields {
|
|
3124
|
+
/** Whether the developer product should be purchasable. */
|
|
3125
|
+
readonly isForSale: boolean;
|
|
3126
|
+
/** Default price in Robux; absent when the product is off-sale. */
|
|
3127
|
+
readonly price?: number;
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* Translate a Mantle-style optional price into the Open Cloud wire shape.
|
|
2836
3131
|
*
|
|
2837
|
-
* `
|
|
2838
|
-
*
|
|
2839
|
-
*
|
|
2840
|
-
*
|
|
3132
|
+
* `desired.price === undefined` (no price declared) becomes
|
|
3133
|
+
* `{ isForSale: false }` and the `price` key is omitted entirely. A defined
|
|
3134
|
+
* price (including `0`) becomes `{ isForSale: true, price }`. Both
|
|
3135
|
+
* `developerProduct` create and update paths share this helper so the
|
|
3136
|
+
* "absent ⇒ off-sale" semantics live in exactly one place.
|
|
3137
|
+
*
|
|
3138
|
+
* @param desired - Object carrying the user-declared `price`.
|
|
3139
|
+
* @returns The wire-shape `{ isForSale, price? }` fragment.
|
|
2841
3140
|
*
|
|
2842
3141
|
* @example
|
|
2843
3142
|
*
|
|
2844
3143
|
* ```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
|
-
* };
|
|
3144
|
+
* import { derivePriceFields } from "@bedrock-rbx/core";
|
|
2863
3145
|
*
|
|
2864
|
-
* expect(
|
|
3146
|
+
* expect(derivePriceFields({ price: undefined })).toStrictEqual({ isForSale: false });
|
|
3147
|
+
* expect(derivePriceFields({ price: 250 })).toStrictEqual({ isForSale: true, price: 250 });
|
|
2865
3148
|
* ```
|
|
2866
3149
|
*/
|
|
2867
|
-
|
|
2868
|
-
readonly
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
} | {
|
|
2873
|
-
readonly appliedSoFar: ReadonlyArray<ResourceCurrentState>;
|
|
2874
|
-
readonly key: ResourceKey;
|
|
2875
|
-
readonly kind: "updateUnsupported";
|
|
2876
|
-
};
|
|
3150
|
+
declare function derivePriceFields(desired: {
|
|
3151
|
+
readonly price: number | undefined;
|
|
3152
|
+
}): PriceFields;
|
|
3153
|
+
//#endregion
|
|
3154
|
+
//#region src/core/diff.d.ts
|
|
2877
3155
|
/**
|
|
2878
|
-
*
|
|
2879
|
-
*
|
|
2880
|
-
* `updateUnsupported`), the remaining operations are skipped and the error
|
|
2881
|
-
* is returned verbatim.
|
|
3156
|
+
* Computes the operations required to reconcile `current` state with `desired`
|
|
3157
|
+
* state. Pure and synchronous: no I/O, no side effects, no `Result` wrapper.
|
|
2882
3158
|
*
|
|
2883
|
-
*
|
|
2884
|
-
*
|
|
2885
|
-
*
|
|
2886
|
-
*
|
|
2887
|
-
*
|
|
2888
|
-
*
|
|
3159
|
+
* Each entry in `desired` is matched to `current` by `(kind, key)`: resources
|
|
3160
|
+
* are uniquely identified by that pair, so a `place` and a `universe` keyed
|
|
3161
|
+
* `"main"` are independent slots. A `(kind, key)` pair present only in
|
|
3162
|
+
* `desired` produces a `create` op; a pair present in both produces an
|
|
3163
|
+
* `update` op if any declared field differs or a `noop` op if every field
|
|
3164
|
+
* matches.
|
|
2889
3165
|
*
|
|
2890
|
-
*
|
|
2891
|
-
*
|
|
2892
|
-
*
|
|
2893
|
-
* state
|
|
3166
|
+
* Ops appear in the order their desired entries appear in the input array so
|
|
3167
|
+
* callers can rely on declaration order when logging or applying ops.
|
|
3168
|
+
*
|
|
3169
|
+
* @param desired - Declared desired state from user config, already normalized
|
|
3170
|
+
* (file hashes computed, nullable wire values mapped to `undefined`).
|
|
3171
|
+
* @param current - Last-known live state from the state file.
|
|
3172
|
+
* @returns Operations to reconcile the two snapshots.
|
|
2894
3173
|
*
|
|
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
3174
|
* @example
|
|
2906
3175
|
*
|
|
2907
3176
|
* ```ts
|
|
2908
3177
|
* import {
|
|
2909
|
-
* applyOps,
|
|
2910
3178
|
* asResourceKey,
|
|
2911
3179
|
* asRobloxAssetId,
|
|
2912
3180
|
* asSha256Hex,
|
|
2913
|
-
*
|
|
2914
|
-
* type
|
|
3181
|
+
* diff,
|
|
3182
|
+
* type GamePassDesiredState,
|
|
3183
|
+
* type ResourceCurrentState,
|
|
2915
3184
|
* } from "@bedrock-rbx/core";
|
|
2916
3185
|
*
|
|
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
|
-
* },
|
|
3186
|
+
* const hash = asSha256Hex(
|
|
3187
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
3188
|
+
* );
|
|
3189
|
+
*
|
|
3190
|
+
* const unchanged: GamePassDesiredState = {
|
|
3191
|
+
* description: "Grants VIP perks.",
|
|
3192
|
+
* icon: { "en-us": "assets/vip-icon.png" },
|
|
3193
|
+
* iconFileHashes: { "en-us": hash },
|
|
3194
|
+
* key: asResourceKey("vip-pass"),
|
|
3195
|
+
* kind: "gamePass",
|
|
3196
|
+
* name: "VIP Pass",
|
|
3197
|
+
* price: 500,
|
|
3198
|
+
* };
|
|
3199
|
+
* const drifted: GamePassDesiredState = {
|
|
3200
|
+
* ...unchanged,
|
|
3201
|
+
* key: asResourceKey("legend-pass"),
|
|
3202
|
+
* name: "Legend Pass (renamed)",
|
|
3203
|
+
* };
|
|
3204
|
+
* const fresh: GamePassDesiredState = {
|
|
3205
|
+
* ...unchanged,
|
|
3206
|
+
* key: asResourceKey("rookie-pass"),
|
|
3207
|
+
* name: "Rookie Pass",
|
|
2959
3208
|
* };
|
|
2960
3209
|
*
|
|
2961
|
-
* const
|
|
3210
|
+
* const current: ReadonlyArray<ResourceCurrentState> = [
|
|
2962
3211
|
* {
|
|
2963
|
-
*
|
|
2964
|
-
*
|
|
2965
|
-
*
|
|
2966
|
-
*
|
|
2967
|
-
*
|
|
2968
|
-
*
|
|
2969
|
-
*
|
|
2970
|
-
*
|
|
2971
|
-
*
|
|
2972
|
-
*
|
|
2973
|
-
*
|
|
2974
|
-
* },
|
|
2975
|
-
* kind: "gamePass",
|
|
2976
|
-
* price: 500,
|
|
3212
|
+
* ...unchanged,
|
|
3213
|
+
* outputs: {
|
|
3214
|
+
* assetId: asRobloxAssetId("111"),
|
|
3215
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("222") },
|
|
3216
|
+
* },
|
|
3217
|
+
* },
|
|
3218
|
+
* {
|
|
3219
|
+
* ...drifted,
|
|
3220
|
+
* name: "Legend Pass",
|
|
3221
|
+
* outputs: {
|
|
3222
|
+
* assetId: asRobloxAssetId("333"),
|
|
3223
|
+
* iconAssetIds: { "en-us": asRobloxAssetId("444") },
|
|
2977
3224
|
* },
|
|
2978
3225
|
* },
|
|
2979
3226
|
* ];
|
|
2980
3227
|
*
|
|
2981
|
-
*
|
|
2982
|
-
*
|
|
2983
|
-
*
|
|
2984
|
-
* });
|
|
3228
|
+
* const ops = diff([unchanged, drifted, fresh], current);
|
|
3229
|
+
*
|
|
3230
|
+
* expect(ops.map((op) => op.type)).toEqual(["noop", "update", "create"]);
|
|
2985
3231
|
* ```
|
|
2986
3232
|
*/
|
|
2987
|
-
declare function
|
|
3233
|
+
declare function diff(desired: ReadonlyArray<ResourceDesiredState>, current: ReadonlyArray<ResourceCurrentState>): ReadonlyArray<Operation>;
|
|
2988
3234
|
//#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
|
-
}
|
|
3235
|
+
//#region src/core/display-name-prefix.d.ts
|
|
3004
3236
|
/**
|
|
3005
|
-
*
|
|
3006
|
-
*
|
|
3007
|
-
*
|
|
3237
|
+
* Default template applied when a project enables display-name prefixing
|
|
3238
|
+
* without supplying its own `displayNamePrefix.format`. Yields outputs
|
|
3239
|
+
* like `[STAGING] ` for an environment whose `label` is `"staging"`.
|
|
3008
3240
|
*/
|
|
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
|
-
}
|
|
3241
|
+
declare const DEFAULT_PREFIX_FORMAT = "[{LABEL}] ";
|
|
3026
3242
|
/**
|
|
3027
|
-
*
|
|
3028
|
-
*
|
|
3029
|
-
*
|
|
3030
|
-
*
|
|
3243
|
+
* Render the prefix that selectEnvironment prepends to declared display
|
|
3244
|
+
* names when a project enables `displayNamePrefix`. The template
|
|
3245
|
+
* recognizes three placeholders:
|
|
3246
|
+
*
|
|
3247
|
+
* - `{label}`: label as written.
|
|
3248
|
+
* - `{LABEL}`: upper-cased label.
|
|
3249
|
+
* - `{Label}`: capitalized label (first character upper, rest as written).
|
|
3250
|
+
*
|
|
3251
|
+
* Other characters in the template flow through verbatim.
|
|
3252
|
+
*
|
|
3253
|
+
* @param label - Environment label declared on `EnvironmentEntry.label`.
|
|
3254
|
+
* @param format - Template string. Falls back to
|
|
3255
|
+
* {@link DEFAULT_PREFIX_FORMAT} when omitted.
|
|
3256
|
+
* @returns The rendered prefix string.
|
|
3031
3257
|
*
|
|
3032
3258
|
* @example
|
|
3033
3259
|
*
|
|
3034
3260
|
* ```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
|
-
* });
|
|
3261
|
+
* import { renderDisplayNamePrefix } from "@bedrock-rbx/core";
|
|
3043
3262
|
*
|
|
3044
|
-
* expect(
|
|
3263
|
+
* expect(renderDisplayNamePrefix("staging")).toBe("[STAGING] ");
|
|
3264
|
+
* expect(renderDisplayNamePrefix("staging", "{Label}: ")).toBe("Staging: ");
|
|
3265
|
+
* expect(renderDisplayNamePrefix("dev", "{LABEL}-{label}")).toBe("DEV-dev");
|
|
3045
3266
|
* ```
|
|
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
3267
|
*/
|
|
3051
|
-
declare function
|
|
3268
|
+
declare function renderDisplayNamePrefix(label: string, format?: string): string;
|
|
3052
3269
|
//#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
|
-
}
|
|
3270
|
+
//#region src/core/environment.d.ts
|
|
3077
3271
|
/**
|
|
3078
|
-
*
|
|
3079
|
-
*
|
|
3080
|
-
*
|
|
3081
|
-
*
|
|
3272
|
+
* Validate an environment name at a state-adapter boundary.
|
|
3273
|
+
*
|
|
3274
|
+
* Adapters that map environment names onto filesystem-like identifiers
|
|
3275
|
+
* (gist filenames, S3 keys) must reject names that could collide or escape
|
|
3276
|
+
* their storage layout. This helper accepts letters, digits, `-`, and `_`
|
|
3277
|
+
* only, with length between 1 and 64, and returns a `StateError` for
|
|
3278
|
+
* anything outside that set so the adapter can fail loudly instead of
|
|
3279
|
+
* silently stripping characters.
|
|
3082
3280
|
*
|
|
3083
3281
|
* @example
|
|
3084
3282
|
*
|
|
3085
3283
|
* ```ts
|
|
3086
|
-
* import {
|
|
3284
|
+
* import { validateEnvironmentName } from "@bedrock-rbx/core";
|
|
3087
3285
|
*
|
|
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
|
-
* });
|
|
3286
|
+
* const ok = validateEnvironmentName("production");
|
|
3287
|
+
* expect(ok.success).toBeTrue();
|
|
3097
3288
|
*
|
|
3098
|
-
*
|
|
3289
|
+
* const bad = validateEnvironmentName("prod/staging");
|
|
3290
|
+
* expect(bad.success).toBeFalse();
|
|
3099
3291
|
* ```
|
|
3100
3292
|
*
|
|
3101
|
-
* @param
|
|
3102
|
-
* @returns
|
|
3103
|
-
*
|
|
3293
|
+
* @param environment - Raw environment name supplied by a caller.
|
|
3294
|
+
* @returns `Ok(environment)` when the name is safe to use, or
|
|
3295
|
+
* `Err(StateError)` with a descriptive reason when it is not.
|
|
3104
3296
|
*/
|
|
3105
|
-
declare function
|
|
3297
|
+
declare function validateEnvironmentName(environment: string): Result$1<string, StateError>;
|
|
3106
3298
|
//#endregion
|
|
3107
|
-
//#region src/
|
|
3299
|
+
//#region src/core/get-environment.d.ts
|
|
3108
3300
|
/**
|
|
3109
|
-
*
|
|
3110
|
-
|
|
3301
|
+
* Failure modes returned by {@link getEnvironment}.
|
|
3302
|
+
*/
|
|
3303
|
+
type GetEnvironmentError = {
|
|
3304
|
+
readonly kind: "missingEnvironment";
|
|
3305
|
+
} | {
|
|
3306
|
+
readonly kind: "multipleEnvironments";
|
|
3307
|
+
readonly values: ReadonlyArray<string>;
|
|
3308
|
+
};
|
|
3309
|
+
/**
|
|
3310
|
+
* Resolve the deploy environment for an override script invocation.
|
|
3111
3311
|
*
|
|
3112
|
-
*
|
|
3113
|
-
*
|
|
3114
|
-
*
|
|
3115
|
-
*
|
|
3312
|
+
* Reads `--env <name>` from the supplied argv first, falls back to
|
|
3313
|
+
* `BEDROCK_ENVIRONMENT` from the supplied env reader. Returns
|
|
3314
|
+
* `missingEnvironment` when neither is present and `multipleEnvironments`
|
|
3315
|
+
* (with every offending value) when argv contains more than one `--env`
|
|
3316
|
+
* flag. Both inputs default to the running process so override scripts
|
|
3317
|
+
* under `.bedrock/` can call `getEnvironment()` with no arguments.
|
|
3116
3318
|
*
|
|
3117
|
-
* @param
|
|
3118
|
-
*
|
|
3119
|
-
*
|
|
3120
|
-
*
|
|
3121
|
-
*
|
|
3319
|
+
* @param argv - Argument list to scan for `--env <name>` flags. Defaults to
|
|
3320
|
+
* `process.argv.slice(2)` when omitted.
|
|
3321
|
+
* @param readEnvironment - Reads an environment variable; consulted as a
|
|
3322
|
+
* fallback when no `--env` flag is present. Defaults to a `process.env`
|
|
3323
|
+
* reader when omitted.
|
|
3324
|
+
* @returns `Ok(environment)` on success, `Err(GetEnvironmentError)` otherwise.
|
|
3122
3325
|
* @example
|
|
3123
3326
|
*
|
|
3124
3327
|
* ```ts
|
|
3125
|
-
* import {
|
|
3328
|
+
* import { getEnvironment } from "@bedrock-rbx/core";
|
|
3126
3329
|
*
|
|
3127
|
-
*
|
|
3128
|
-
* return new Uint8Array([1, 2, 3]);
|
|
3129
|
-
* }
|
|
3330
|
+
* const result = getEnvironment(["--env", "production"], () => undefined);
|
|
3130
3331
|
*
|
|
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
|
-
* });
|
|
3332
|
+
* expect(result.success).toBeTrue();
|
|
3333
|
+
* if (result.success) {
|
|
3334
|
+
* expect(result.data).toBe("production");
|
|
3335
|
+
* }
|
|
3150
3336
|
* ```
|
|
3151
3337
|
*/
|
|
3152
|
-
declare function
|
|
3338
|
+
declare function getEnvironment(argv?: ReadonlyArray<string>, readEnvironment?: (name: string) => string | undefined): Result$1<string, GetEnvironmentError>;
|
|
3153
3339
|
//#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
|
-
}
|
|
3340
|
+
//#region src/core/icons.d.ts
|
|
3174
3341
|
/**
|
|
3175
|
-
*
|
|
3176
|
-
*
|
|
3177
|
-
*
|
|
3178
|
-
*
|
|
3179
|
-
*
|
|
3180
|
-
* long-running scripts see up-to-date values.
|
|
3342
|
+
* Cost-gate for icon re-uploads. Returns `true` when the locally-hashed
|
|
3343
|
+
* desired icon differs from the hash recorded on the prior current-state
|
|
3344
|
+
* entry, signalling that the driver must re-upload before reconciling.
|
|
3345
|
+
* Returns `false` when the hashes match (no re-upload needed) and when
|
|
3346
|
+
* both sides report no icon.
|
|
3181
3347
|
*
|
|
3182
|
-
*
|
|
3183
|
-
*
|
|
3184
|
-
*
|
|
3348
|
+
* The signature takes hash maps directly (not whole-state) so the helper
|
|
3349
|
+
* is independent of any specific resource-kind shape; every icon-bearing
|
|
3350
|
+
* driver projects its own `iconFileHashes` and `outputs.iconFileHashes`
|
|
3351
|
+
* fields before calling.
|
|
3185
3352
|
*
|
|
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.
|
|
3353
|
+
* @param currentHashes - Hashes recorded on the prior current-state entry.
|
|
3354
|
+
* @param desiredHashes - Hashes layered onto the desired-state entry by
|
|
3355
|
+
* `normalize` from the local icon file's bytes.
|
|
3356
|
+
* @returns `true` when the driver should re-upload the icon.
|
|
3194
3357
|
*
|
|
3195
|
-
* @param options - Loader options.
|
|
3196
|
-
* @returns `Ok` with the validated `Config`, or `Err` with a `ConfigError`.
|
|
3197
3358
|
* @example
|
|
3198
3359
|
*
|
|
3199
3360
|
* ```ts
|
|
3200
|
-
* import {
|
|
3361
|
+
* import { asSha256Hex, shouldReuploadIcon } from "@bedrock-rbx/core";
|
|
3201
3362
|
*
|
|
3202
|
-
*
|
|
3203
|
-
*
|
|
3204
|
-
*
|
|
3205
|
-
*
|
|
3206
|
-
*
|
|
3207
|
-
*
|
|
3208
|
-
*
|
|
3209
|
-
*
|
|
3210
|
-
* });
|
|
3363
|
+
* const previous = asSha256Hex(
|
|
3364
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
3365
|
+
* );
|
|
3366
|
+
* const fresh = asSha256Hex(
|
|
3367
|
+
* "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
|
|
3368
|
+
* );
|
|
3369
|
+
*
|
|
3370
|
+
* expect(shouldReuploadIcon({ "en-us": previous }, { "en-us": previous })).toBe(false);
|
|
3371
|
+
* expect(shouldReuploadIcon({ "en-us": previous }, { "en-us": fresh })).toBe(true);
|
|
3211
3372
|
* ```
|
|
3212
3373
|
*/
|
|
3213
|
-
declare function
|
|
3374
|
+
declare function shouldReuploadIcon(currentHashes: Record<"en-us", Sha256Hex> | undefined, desiredHashes: Record<"en-us", Sha256Hex> | undefined): boolean;
|
|
3214
3375
|
//#endregion
|
|
3215
|
-
//#region src/
|
|
3376
|
+
//#region src/core/kinds/index.d.ts
|
|
3216
3377
|
/**
|
|
3217
|
-
*
|
|
3218
|
-
*
|
|
3219
|
-
*
|
|
3378
|
+
* Default {@link KindRegistry} composing every resource kind bedrock ships
|
|
3379
|
+
* out of the box. Iteration order (`gamePass`, `place`, `universe`,
|
|
3380
|
+
* `developerProduct`) matches the order `flattenConfig` emits entries
|
|
3381
|
+
* today, preserving the observable order of generated operations.
|
|
3382
|
+
*
|
|
3383
|
+
* @example
|
|
3384
|
+
*
|
|
3385
|
+
* ```ts
|
|
3386
|
+
* import { defaultKindRegistry } from "@bedrock-rbx/core";
|
|
3387
|
+
*
|
|
3388
|
+
* expect(defaultKindRegistry.gamePass.kind).toBe("gamePass");
|
|
3389
|
+
* expect(defaultKindRegistry.place.kind).toBe("place");
|
|
3390
|
+
* expect(defaultKindRegistry.universe.kind).toBe("universe");
|
|
3391
|
+
* expect(defaultKindRegistry.developerProduct.kind).toBe("developerProduct");
|
|
3392
|
+
* ```
|
|
3220
3393
|
*/
|
|
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
|
-
}
|
|
3394
|
+
declare const defaultKindRegistry: KindRegistry;
|
|
3395
|
+
//#endregion
|
|
3396
|
+
//#region src/core/state-file.d.ts
|
|
3239
3397
|
/**
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3242
|
-
*
|
|
3243
|
-
*
|
|
3244
|
-
*
|
|
3245
|
-
*
|
|
3398
|
+
* Serialize a {@link BedrockState} to the on-disk JSON representation used by
|
|
3399
|
+
* state-port adapters.
|
|
3400
|
+
*
|
|
3401
|
+
* The on-disk shape wraps the in-memory state with a
|
|
3402
|
+
* `$bedrock: { version: N }` envelope so that a future breaking change to the
|
|
3403
|
+
* schema can be detected and rejected at parse time rather than silently
|
|
3404
|
+
* accepted. The top-level `version` field is not duplicated on disk.
|
|
3405
|
+
*
|
|
3406
|
+
* @example
|
|
3407
|
+
*
|
|
3408
|
+
* ```ts
|
|
3409
|
+
* import { serializeStateFile, type BedrockState } from "@bedrock-rbx/core";
|
|
3410
|
+
*
|
|
3411
|
+
* const state: BedrockState = {
|
|
3412
|
+
* environment: "production",
|
|
3413
|
+
* resources: [],
|
|
3414
|
+
* version: 1,
|
|
3415
|
+
* };
|
|
3416
|
+
*
|
|
3417
|
+
* const wire = serializeStateFile(state);
|
|
3418
|
+
* expect(JSON.parse(wire)).toStrictEqual({
|
|
3419
|
+
* $bedrock: { version: 1 },
|
|
3420
|
+
* environment: "production",
|
|
3421
|
+
* resources: [],
|
|
3422
|
+
* });
|
|
3423
|
+
* ```
|
|
3424
|
+
*
|
|
3425
|
+
* @param state - The in-memory state snapshot to serialize.
|
|
3426
|
+
* @returns A pretty-printed JSON string ready to hand to a state adapter's write method.
|
|
3246
3427
|
*/
|
|
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
|
-
};
|
|
3428
|
+
declare function serializeStateFile(state: BedrockState): string;
|
|
3264
3429
|
/**
|
|
3265
|
-
*
|
|
3266
|
-
*
|
|
3267
|
-
*
|
|
3268
|
-
* `
|
|
3430
|
+
* Parse a raw on-disk state file into a {@link BedrockState}.
|
|
3431
|
+
*
|
|
3432
|
+
* A backend that reports "no state file for this environment yet" must pass
|
|
3433
|
+
* `undefined`: that distinguishes a legitimate first deploy from a file that
|
|
3434
|
+
* exists but cannot be trusted.
|
|
3269
3435
|
*
|
|
3270
|
-
* @param options - Target environment plus optional overrides.
|
|
3271
|
-
* @returns The persisted `BedrockState` on success, or a stage-tagged
|
|
3272
|
-
* `DeployError` on failure.
|
|
3273
3436
|
* @example
|
|
3274
3437
|
*
|
|
3275
3438
|
* ```ts
|
|
3276
|
-
* import {
|
|
3439
|
+
* import { parseStateFile } from "@bedrock-rbx/core";
|
|
3277
3440
|
*
|
|
3278
|
-
*
|
|
3279
|
-
*
|
|
3280
|
-
*
|
|
3281
|
-
*
|
|
3282
|
-
*
|
|
3283
|
-
* });
|
|
3441
|
+
* const freshStart = parseStateFile(undefined, "gist:abc123/state.production.json");
|
|
3442
|
+
* expect(freshStart.success).toBeTrue();
|
|
3443
|
+
* if (freshStart.success) {
|
|
3444
|
+
* expect(freshStart.data).toBeUndefined();
|
|
3445
|
+
* }
|
|
3284
3446
|
* ```
|
|
3285
3447
|
*
|
|
3448
|
+
* @param raw - Raw file contents as a string, or `undefined` when the
|
|
3449
|
+
* backend reports no file exists yet.
|
|
3450
|
+
* @param file - Adapter-specific identifier included in any `StateError`
|
|
3451
|
+
* surfaced during parsing.
|
|
3452
|
+
* @returns `Ok(undefined)` for a missing file, `Ok(state)` for a parseable
|
|
3453
|
+
* file, or `Err(StateError)` for anything that cannot be trusted.
|
|
3454
|
+
*/
|
|
3455
|
+
declare function parseStateFile(raw: string | undefined, file: string): Result$1<BedrockState | undefined, StateError>;
|
|
3456
|
+
//#endregion
|
|
3457
|
+
//#region src/core/validate-plan.d.ts
|
|
3458
|
+
/**
|
|
3459
|
+
* Plan-time invariant check that runs after `buildDesired` and before
|
|
3460
|
+
* `diff`. Walks paired `(kind, key)` entries and dispatches to each
|
|
3461
|
+
* kind module's optional `assertReconcilable` hook so kind-specific
|
|
3462
|
+
* rejections (e.g. Removing a developer-product icon, which the upstream
|
|
3463
|
+
* API has no documented unset path for) surface as typed errors before
|
|
3464
|
+
* `diff` runs and before any apply-side driver I/O is attempted.
|
|
3465
|
+
*
|
|
3466
|
+
* Pure and synchronous. Current-only entries (no matching desired) are
|
|
3467
|
+
* ignored: their reconciliation is `diff`'s concern, not this seam's.
|
|
3468
|
+
*
|
|
3469
|
+
* @param desired - Desired state from `buildDesired`.
|
|
3470
|
+
* @param current - Prior current state from the state port.
|
|
3471
|
+
* @returns `Ok(undefined)` when every paired entry passes its kind-level
|
|
3472
|
+
* reconcilability check, or the first `Err` returned by a hook.
|
|
3473
|
+
*
|
|
3286
3474
|
* @example
|
|
3287
3475
|
*
|
|
3288
3476
|
* ```ts
|
|
3289
|
-
* import {
|
|
3477
|
+
* import { asResourceKey, asRobloxAssetId, asSha256Hex, validatePlan } from "@bedrock-rbx/core";
|
|
3290
3478
|
*
|
|
3291
|
-
* const
|
|
3292
|
-
*
|
|
3293
|
-
*
|
|
3294
|
-
*
|
|
3295
|
-
*
|
|
3296
|
-
*
|
|
3297
|
-
*
|
|
3298
|
-
*
|
|
3299
|
-
*
|
|
3300
|
-
*
|
|
3301
|
-
*
|
|
3302
|
-
*
|
|
3303
|
-
*
|
|
3304
|
-
*
|
|
3305
|
-
*
|
|
3306
|
-
*
|
|
3307
|
-
*
|
|
3308
|
-
*
|
|
3479
|
+
* const result = validatePlan(
|
|
3480
|
+
* [
|
|
3481
|
+
* {
|
|
3482
|
+
* description: "Stocks the player up with 1,000 premium gems.",
|
|
3483
|
+
* isRegionalPricingEnabled: undefined,
|
|
3484
|
+
* key: asResourceKey("gem-pack"),
|
|
3485
|
+
* kind: "developerProduct",
|
|
3486
|
+
* name: "Gem Pack",
|
|
3487
|
+
* price: undefined,
|
|
3488
|
+
* storePageEnabled: undefined,
|
|
3489
|
+
* },
|
|
3490
|
+
* ],
|
|
3491
|
+
* [
|
|
3492
|
+
* {
|
|
3493
|
+
* description: "Stocks the player up with 1,000 premium gems.",
|
|
3494
|
+
* icon: { "en-us": "assets/gem-pack.png" },
|
|
3495
|
+
* iconFileHashes: {
|
|
3496
|
+
* "en-us": asSha256Hex(
|
|
3497
|
+
* "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
3498
|
+
* ),
|
|
3499
|
+
* },
|
|
3500
|
+
* isRegionalPricingEnabled: undefined,
|
|
3501
|
+
* key: asResourceKey("gem-pack"),
|
|
3502
|
+
* kind: "developerProduct",
|
|
3503
|
+
* name: "Gem Pack",
|
|
3504
|
+
* outputs: { productId: asRobloxAssetId("9876543210") },
|
|
3505
|
+
* price: undefined,
|
|
3506
|
+
* storePageEnabled: undefined,
|
|
3507
|
+
* },
|
|
3508
|
+
* ],
|
|
3509
|
+
* );
|
|
3309
3510
|
*
|
|
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
|
-
* });
|
|
3511
|
+
* expect(result.success).toBeFalse();
|
|
3512
|
+
* if (!result.success) {
|
|
3513
|
+
* expect(result.err.kind).toBe("iconRemovalRejected");
|
|
3514
|
+
* }
|
|
3326
3515
|
* ```
|
|
3327
3516
|
*/
|
|
3328
|
-
declare function
|
|
3517
|
+
declare function validatePlan(desired: ReadonlyArray<ResourceDesiredState>, current: ReadonlyArray<ResourceCurrentState>): Result$1<undefined, BuildDesiredError>;
|
|
3329
3518
|
//#endregion
|
|
3330
3519
|
//#region src/shell/migrate-mantle-state.d.ts
|
|
3331
3520
|
type ConfigFormat = "typescript" | "yaml";
|
|
@@ -3422,5 +3611,5 @@ interface MigrateMantleStateDeps {
|
|
|
3422
3611
|
*/
|
|
3423
3612
|
declare function migrateMantleState(deps: MigrateMantleStateDeps): Promise<Result$1<MigrationReport, MigrateError>>;
|
|
3424
3613
|
//#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 };
|
|
3614
|
+
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 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
3615
|
//# sourceMappingURL=index.d.mts.map
|