@mpgd/target-config 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { AdPlacementEntry, AdPlacements, ProductCatalog, ProductCatalogEntry } from '@mpgd/catalog';
1
+ import { type AdPlacementEntry, type AdPlacements, type ProductCatalog, type ProductCatalogEntry } from '@mpgd/catalog';
2
2
  import { type FeatureAvailabilityReason, type TargetCapabilityConfig, type TargetConfig, type TargetConfigMatrix, type TargetFeatureConfig, type TargetIntegrationConfig, type TargetLocalizationConfig, type TargetPolicyRestrictions, type TargetReleaseConfig, type TargetRuntimeKind } from './runtime.js';
3
3
  export type EffectiveAvailabilityReason = FeatureAvailabilityReason | 'missing-platform-id';
4
4
  export interface EffectivePlatformTargetMetadata {
package/dist/effective.js CHANGED
@@ -1,4 +1,5 @@
1
- import { normalizeTargetIntegrationConfig, } from './runtime.js';
1
+ import { resolveAdPlacementPlatformId, resolveProductPlatformId, } from '@mpgd/catalog';
2
+ import { assertTargetIntegrationRuntimeBounds, } from './runtime.js';
2
3
  export const defaultLeaderboardId = 'default';
3
4
  const disabledAuthoritativeMonetization = {
4
5
  iap: false,
@@ -10,10 +11,10 @@ export function createEffectiveTargetConfig(input) {
10
11
  const products = input.catalog.products.map((product) => createEffectiveProductConfig(input.target, config, product));
11
12
  const placements = input.adPlacements.placements.map((placement) => createEffectiveAdPlacementConfig(input.target, config, placement));
12
13
  const leaderboardEnabled = config.features.leaderboard;
13
- const integrations = normalizeTargetIntegrationConfig({
14
+ const integrations = assertTargetIntegrationRuntimeBounds(config.runtime, {
14
15
  ...config.integrations,
15
16
  ...input.platformTarget?.integrations,
16
- });
17
+ }, `Effective target ${input.target}`);
17
18
  return {
18
19
  version: effectiveTargetConfigVersion({
19
20
  targetConfig: input.targetConfigVersion,
@@ -107,8 +108,8 @@ export function getEffectiveAdPlacementConfig(config, placementId) {
107
108
  return config.ads.placements.find((placement) => placement.id === placementId);
108
109
  }
109
110
  function createEffectiveProductConfig(target, config, product) {
110
- const platformProductId = productPlatformId(product, target);
111
- const reason = effectiveProductReason(target, config.features.iap, product, platformProductId);
111
+ const platformProductId = resolveProductPlatformId(product, target);
112
+ const reason = effectiveProductReason(config.runtime, config.features.iap, product, platformProductId);
112
113
  return {
113
114
  id: product.id,
114
115
  type: product.type,
@@ -118,11 +119,11 @@ function createEffectiveProductConfig(target, config, product) {
118
119
  ...(platformProductId === undefined ? {} : { platformProductId }),
119
120
  };
120
121
  }
121
- function effectiveProductReason(target, targetEnabled, product, platformProductId) {
122
+ function effectiveProductReason(runtime, targetEnabled, product, platformProductId) {
122
123
  if (!targetEnabled) {
123
124
  return 'target-disabled';
124
125
  }
125
- if (target === 'verse8' && product.grant.type === 'resource') {
126
+ if (runtime === 'verse8-web' && product.grant.type === 'resource') {
126
127
  return 'capability-unsupported';
127
128
  }
128
129
  return effectiveItemReason(targetEnabled, platformProductId);
@@ -131,7 +132,7 @@ function createEffectiveAdPlacementConfig(target, config, placement) {
131
132
  const featureEnabled = isRewardedPlacement(placement)
132
133
  ? config.features.rewardedAds
133
134
  : config.features.interstitialAds;
134
- const platformPlacementId = adPlacementPlatformId(placement, target);
135
+ const platformPlacementId = resolveAdPlacementPlatformId(placement, target);
135
136
  const reason = effectiveItemReason(featureEnabled, platformPlacementId);
136
137
  return {
137
138
  id: placement.id,
@@ -152,35 +153,6 @@ function effectiveItemReason(targetEnabled, platformId) {
152
153
  function effectiveTargetConfigVersion(input) {
153
154
  return `${input.targetConfig}+catalog.${input.productCatalog}+ads.${input.adPlacements}`;
154
155
  }
155
- function productPlatformId(product, target) {
156
- if (!isProductStoreTarget(target)) {
157
- return undefined;
158
- }
159
- return normalizePlatformId(product.platformProductIds[target]);
160
- }
161
- function adPlacementPlatformId(placement, target) {
162
- if (!isAdStoreTarget(target)) {
163
- return undefined;
164
- }
165
- return normalizePlatformId(placement.platformPlacementIds[target]);
166
- }
167
- function normalizePlatformId(platformId) {
168
- if (platformId === undefined) {
169
- return undefined;
170
- }
171
- const trimmed = platformId.trim();
172
- return trimmed.length === 0 ? undefined : trimmed;
173
- }
174
- function isProductStoreTarget(target) {
175
- return target === 'android'
176
- || target === 'ios'
177
- || target === 'ait'
178
- || target === 'reddit'
179
- || target === 'verse8';
180
- }
181
- function isAdStoreTarget(target) {
182
- return target === 'android' || target === 'ios' || target === 'ait' || target === 'verse8';
183
- }
184
156
  function isRewardedPlacement(placement) {
185
157
  return placement.type === 'rewarded';
186
158
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { EffectiveTargetConfigMatrix } from './effective.js';
2
2
  import type { TargetConfigMatrix } from './runtime.js';
3
3
  export * from './effective.js';
4
+ export * from './releaseIdentity.js';
4
5
  export * from './runtime.js';
5
6
  export * from './viewport.js';
6
7
  export declare const assertEffectiveTargetConfigMatrix: (input: unknown) => EffectiveTargetConfigMatrix;
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import * as _accessExpressionAsString_1 from "typia/lib/internal/_accessExpressi
3
3
  const __typia_transform__accessExpressionAsString = _accessExpressionAsString_1._accessExpressionAsString;
4
4
  import typia from 'typia';
5
5
  export * from './effective.js';
6
+ export * from './releaseIdentity.js';
6
7
  export * from './runtime.js';
7
8
  export * from './viewport.js';
8
9
  export const assertEffectiveTargetConfigMatrix = (() => {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A human-facing release identity shared by every target artifact for one game release.
3
+ *
4
+ * `gameVersion` remains the game's SemVer value. `releaseRevision` is deliberately
5
+ * separate so a game can keep one semantic release while targets are promoted or
6
+ * rebuilt independently. For example, the common label for game `0.3.30` and
7
+ * revision `42` is `0.3.30-v42`.
8
+ */
9
+ export interface MpgdReleaseIdentity {
10
+ readonly gameVersion: string;
11
+ readonly releaseRevision?: number;
12
+ readonly label: string;
13
+ }
14
+ export interface CreateMpgdReleaseIdentityInput {
15
+ readonly gameVersion: string;
16
+ readonly releaseRevision?: number;
17
+ /**
18
+ * Optional value from a build environment. It must equal the derived label so
19
+ * a stale or hand-edited label cannot be embedded into an artifact.
20
+ */
21
+ readonly expectedLabel?: string;
22
+ }
23
+ export declare function createMpgdReleaseIdentity(input: CreateMpgdReleaseIdentityInput): MpgdReleaseIdentity;
24
+ export declare function formatMpgdReleaseLabel(gameVersion: string, releaseRevision: number | undefined): string;
25
+ /** Whether a normalized game version is eligible for an immutable production release. */
26
+ export declare function isMpgdFinalSemVer(value: string): boolean;
27
+ /**
28
+ * Canonical release-manifest identifier derived from a common release label.
29
+ * Both components must be non-empty canonical tokens without the `+` delimiter.
30
+ */
31
+ export declare function formatMpgdReleaseId(label: string, buildId: string): string;
32
+ /** Parse an optional release revision without making empty values meaningful. */
33
+ export declare function parseMpgdReleaseRevision(value: string | undefined): number | undefined;
@@ -0,0 +1,80 @@
1
+ const semVerCore = '(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)';
2
+ const preReleaseNumericIdentifier = '(?:0|[1-9]\\d*)';
3
+ const preReleaseAlphanumericIdentifier = '(?:\\d*[A-Za-z-][0-9A-Za-z-]*)';
4
+ const preReleaseIdentifier = `(?:${preReleaseNumericIdentifier}|${preReleaseAlphanumericIdentifier})`;
5
+ const finalSemVerPattern = new RegExp(`^${semVerCore}$`, 'u');
6
+ const semVerPattern = new RegExp(`^${semVerCore}(?:-${preReleaseIdentifier}(?:\\.${preReleaseIdentifier})*)?$`, 'u');
7
+ export function createMpgdReleaseIdentity(input) {
8
+ const gameVersion = normalizeMpgdSemVer(input.gameVersion);
9
+ const releaseRevision = input.releaseRevision;
10
+ const label = formatNormalizedMpgdReleaseLabel(gameVersion, releaseRevision);
11
+ const expectedLabel = input.expectedLabel?.trim();
12
+ if (expectedLabel !== undefined && expectedLabel !== label) {
13
+ const detail = `${label}; received ${expectedLabel}.`;
14
+ throw new TypeError(`Release identity label must equal ${detail}`);
15
+ }
16
+ return releaseRevision === undefined
17
+ ? { gameVersion, label }
18
+ : { gameVersion, releaseRevision, label };
19
+ }
20
+ export function formatMpgdReleaseLabel(gameVersion, releaseRevision) {
21
+ return formatNormalizedMpgdReleaseLabel(normalizeMpgdSemVer(gameVersion), releaseRevision);
22
+ }
23
+ function formatNormalizedMpgdReleaseLabel(gameVersion, releaseRevision) {
24
+ if (releaseRevision === undefined) {
25
+ return gameVersion;
26
+ }
27
+ if (!isMpgdFinalSemVer(gameVersion)) {
28
+ throw new TypeError('Release identity gameVersion must be a final SemVer when releaseRevision is set.');
29
+ }
30
+ if (!Number.isSafeInteger(releaseRevision) || releaseRevision < 1) {
31
+ throw new TypeError('Release identity releaseRevision must be a positive safe integer.');
32
+ }
33
+ return `${gameVersion}-v${releaseRevision}`;
34
+ }
35
+ /** Whether a normalized game version is eligible for an immutable production release. */
36
+ export function isMpgdFinalSemVer(value) {
37
+ return finalSemVerPattern.test(value);
38
+ }
39
+ /**
40
+ * Canonical release-manifest identifier derived from a common release label.
41
+ * Both components must be non-empty canonical tokens without the `+` delimiter.
42
+ */
43
+ export function formatMpgdReleaseId(label, buildId) {
44
+ assertReleaseIdComponent(label, 'label');
45
+ assertReleaseIdComponent(buildId, 'buildId');
46
+ return `mpgd-${label}+${buildId}`;
47
+ }
48
+ /** Parse an optional release revision without making empty values meaningful. */
49
+ export function parseMpgdReleaseRevision(value) {
50
+ if (value === undefined || value.trim().length === 0) {
51
+ return undefined;
52
+ }
53
+ const normalized = value.trim();
54
+ if (!/^[1-9]\d*$/u.test(normalized)) {
55
+ throw new TypeError('Release revision must be a positive safe integer.');
56
+ }
57
+ const releaseRevision = Number.parseInt(normalized, 10);
58
+ if (!Number.isSafeInteger(releaseRevision)) {
59
+ throw new TypeError('Release revision must be a positive safe integer.');
60
+ }
61
+ return releaseRevision;
62
+ }
63
+ function normalizeMpgdSemVer(value) {
64
+ const normalized = value.trim();
65
+ if (normalized.includes('+')) {
66
+ throw new TypeError('Release identity gameVersion must not contain SemVer build metadata because "+" is reserved for release IDs.');
67
+ }
68
+ if (!semVerPattern.test(normalized)) {
69
+ throw new TypeError('Release identity gameVersion must be a SemVer value.');
70
+ }
71
+ return normalized;
72
+ }
73
+ function assertReleaseIdComponent(value, label) {
74
+ if (value.length === 0 || /\s/u.test(value)) {
75
+ throw new TypeError(`Release identifier ${label} must be a non-empty canonical token.`);
76
+ }
77
+ if (value.includes('+')) {
78
+ throw new TypeError(`Release identifier ${label} must not contain "+".`);
79
+ }
80
+ }
package/dist/runtime.d.ts CHANGED
@@ -128,6 +128,7 @@ export declare const defaultTargetIntegrationConfig: {
128
128
  readonly presentationMode: 'fullscreen';
129
129
  };
130
130
  export declare function normalizeTargetIntegrationConfig(config: Partial<TargetIntegrationConfig> | undefined): TargetIntegrationConfig;
131
+ export declare function assertTargetIntegrationRuntimeBounds(runtime: TargetRuntimeKind, config: Partial<TargetIntegrationConfig> | undefined, label: string): TargetIntegrationConfig;
131
132
  export declare function targetConfigKeyForPlatform(target: PlatformConfigTarget): string;
132
133
  export declare function getTargetConfig(matrix: TargetConfigMatrix, target: PlatformConfigTarget | string): TargetConfig;
133
134
  export declare function isPlatformFeatureEnabled(config: TargetConfig, feature: PlatformFeature): boolean;
package/dist/runtime.js CHANGED
@@ -12,6 +12,22 @@ export const targetIntegrations = [
12
12
  'inboundShare',
13
13
  'notifications',
14
14
  ];
15
+ const integrationUpperBoundsByRuntime = {
16
+ 'verse8-web': {
17
+ identityUpgrade: 'unsupported',
18
+ presentation: 'available',
19
+ sharing: 'unsupported',
20
+ inboundShare: 'unsupported',
21
+ notifications: 'unsupported',
22
+ },
23
+ 'web-preview': {
24
+ identityUpgrade: 'disabled',
25
+ presentation: 'available',
26
+ sharing: 'available',
27
+ inboundShare: 'available',
28
+ notifications: 'unsupported',
29
+ },
30
+ };
15
31
  const integrationAvailabilityStateValues = [
16
32
  'available',
17
33
  'disabled',
@@ -48,6 +64,31 @@ export function normalizeTargetIntegrationConfig(config) {
48
64
  presentationMode: normalizePresentationMode(config.presentationMode),
49
65
  };
50
66
  }
67
+ export function assertTargetIntegrationRuntimeBounds(runtime, config, label) {
68
+ const integrations = normalizeTargetIntegrationConfig(config);
69
+ if (runtime !== 'verse8-web' && runtime !== 'web-preview') {
70
+ return integrations;
71
+ }
72
+ const upperBounds = integrationUpperBoundsByRuntime[runtime];
73
+ for (const integration of targetIntegrations) {
74
+ const state = integrations[integration];
75
+ const upperBound = upperBounds[integration];
76
+ if (!isIntegrationStateWithinUpperBound(state, upperBound)) {
77
+ throw new Error(`${label} cannot configure ${integration} as ${state} for ${runtime} runtime; maximum supported state is ${upperBound}.`);
78
+ }
79
+ }
80
+ return integrations;
81
+ }
82
+ function isIntegrationStateWithinUpperBound(state, upperBound) {
83
+ switch (upperBound) {
84
+ case 'available':
85
+ return true;
86
+ case 'disabled':
87
+ return state === 'disabled' || state === 'unsupported';
88
+ case 'unsupported':
89
+ return state === 'unsupported';
90
+ }
91
+ }
51
92
  export function targetConfigKeyForPlatform(target) {
52
93
  return target === 'browser' ? 'web-preview' : target;
53
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/target-config",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Target-specific platform feature availability and effective config helpers for mpgd.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,9 +37,9 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "typia": "13.0.2",
40
- "@mpgd/catalog": "0.5.1",
41
- "@mpgd/i18n": "0.5.4",
42
- "@mpgd/platform": "0.8.0"
40
+ "@mpgd/catalog": "0.5.2",
41
+ "@mpgd/platform": "0.8.0",
42
+ "@mpgd/i18n": "0.5.4"
43
43
  },
44
44
  "devDependencies": {
45
45
  "ttsc": "0.18.4",