diff --git a/.sampo/changesets/target-bundle-purity.md b/.sampo/changesets/target-bundle-purity.md new file mode 100644 index 000000000..c4bcf14e8 --- /dev/null +++ b/.sampo/changesets/target-bundle-purity.md @@ -0,0 +1,5 @@ +--- +"@mpgd/cli": minor +--- + +Generate target-isolated platform gateway entrypoints so production bundles include only the selected platform adapter. diff --git a/examples/phaser-starter/src/platform/buildGatewayModule.ts b/examples/phaser-starter/src/platform/buildGatewayModule.ts new file mode 100644 index 000000000..4daea75e8 --- /dev/null +++ b/examples/phaser-starter/src/platform/buildGatewayModule.ts @@ -0,0 +1,5 @@ +declare module '#mpgd-platform-gateway' { + export function createBuildGateway( + runtime: import('./runtimeDetector').RuntimeConfig, + ): Promise; +} diff --git a/examples/phaser-starter/src/platform/buildGateways/ait.ts b/examples/phaser-starter/src/platform/buildGateways/ait.ts new file mode 100644 index 000000000..a8b945ce5 --- /dev/null +++ b/examples/phaser-starter/src/platform/buildGateways/ait.ts @@ -0,0 +1,11 @@ +import { createAitPlatformGateway } from '@mpgd/adapter-ait'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createAitPlatformGateway({ + appVersion: runtime.appVersion, + buildId: runtime.buildId, + }); +} diff --git a/examples/phaser-starter/src/platform/buildGateways/aitSandbox.ts b/examples/phaser-starter/src/platform/buildGateways/aitSandbox.ts new file mode 100644 index 000000000..1ba1ec900 --- /dev/null +++ b/examples/phaser-starter/src/platform/buildGateways/aitSandbox.ts @@ -0,0 +1,12 @@ +import { createAitPlatformGateway, createAitSandboxBridge } from '@mpgd/adapter-ait'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createAitPlatformGateway({ + appVersion: runtime.appVersion, + buildId: runtime.buildId, + fallbackBridge: createAitSandboxBridge(), + }); +} diff --git a/examples/phaser-starter/src/platform/buildGateways/browser.ts b/examples/phaser-starter/src/platform/buildGateways/browser.ts new file mode 100644 index 000000000..19eb70fe8 --- /dev/null +++ b/examples/phaser-starter/src/platform/buildGateways/browser.ts @@ -0,0 +1,8 @@ +import { createBrowserPlatformGateway } from '@mpgd/adapter-browser'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(_runtime: RuntimeConfig): Promise { + return createBrowserPlatformGateway(); +} diff --git a/examples/phaser-starter/src/platform/buildGateways/capacitorAndroid.ts b/examples/phaser-starter/src/platform/buildGateways/capacitorAndroid.ts new file mode 100644 index 000000000..3d84eb7ad --- /dev/null +++ b/examples/phaser-starter/src/platform/buildGateways/capacitorAndroid.ts @@ -0,0 +1,12 @@ +import { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createCapacitorPlatformGateway({ + target: 'android', + appVersion: runtime.appVersion, + buildId: runtime.buildId, + }); +} diff --git a/examples/phaser-starter/src/platform/buildGateways/capacitorIos.ts b/examples/phaser-starter/src/platform/buildGateways/capacitorIos.ts new file mode 100644 index 000000000..e0a6f715e --- /dev/null +++ b/examples/phaser-starter/src/platform/buildGateways/capacitorIos.ts @@ -0,0 +1,12 @@ +import { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createCapacitorPlatformGateway({ + target: 'ios', + appVersion: runtime.appVersion, + buildId: runtime.buildId, + }); +} diff --git a/examples/phaser-starter/src/platform/buildGateways/reddit.ts b/examples/phaser-starter/src/platform/buildGateways/reddit.ts new file mode 100644 index 000000000..c121f881d --- /dev/null +++ b/examples/phaser-starter/src/platform/buildGateways/reddit.ts @@ -0,0 +1,11 @@ +import { createDevvitPlatformGateway } from '@mpgd/adapter-devvit'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createDevvitPlatformGateway({ + appVersion: runtime.appVersion, + buildId: runtime.buildId, + }); +} diff --git a/examples/phaser-starter/src/platform/buildGateways/redditSandbox.ts b/examples/phaser-starter/src/platform/buildGateways/redditSandbox.ts new file mode 100644 index 000000000..7b4b48c5e --- /dev/null +++ b/examples/phaser-starter/src/platform/buildGateways/redditSandbox.ts @@ -0,0 +1,15 @@ +import { + createDevvitPlatformGateway, + createDevvitSandboxBridge, +} from '@mpgd/adapter-devvit'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createDevvitPlatformGateway({ + appVersion: runtime.appVersion, + buildId: runtime.buildId, + fallbackBridge: createDevvitSandboxBridge(), + }); +} diff --git a/examples/phaser-starter/src/platform/installStarterPlatform.ts b/examples/phaser-starter/src/platform/installStarterPlatform.ts index 924d27dde..07370897c 100644 --- a/examples/phaser-starter/src/platform/installStarterPlatform.ts +++ b/examples/phaser-starter/src/platform/installStarterPlatform.ts @@ -1,6 +1,7 @@ +import { createBuildGateway } from '#mpgd-platform-gateway'; + import type { AdPlacements, ProductCatalog } from '@mpgd/catalog'; import adPlacementsJson from '@mpgd/catalog/placements.json'; -import type { PlatformGateway } from '@mpgd/platform'; import productCatalogJson from '@mpgd/catalog/catalog.json'; import { createEffectiveTargetConfig, @@ -14,7 +15,6 @@ import targetConfigMatrixJson from '@mpgd/target-config/targets.json'; import type { RuntimeConfig } from './runtimeDetector'; -const devvitSandboxBuildId = 'devvit-sandbox'; const targetConfigMatrix = targetConfigMatrixJson as TargetConfigMatrix; const productCatalog = productCatalogJson as ProductCatalog; const adPlacements = adPlacementsJson as AdPlacements; @@ -29,7 +29,7 @@ const adPlacementTypes = new Map( export async function installStarterPlatform( runtime: RuntimeConfig, ): Promise { - const gateway = await createPlatformGateway(runtime); + const gateway = await createBuildGateway(runtime); const configTarget = runtime.configTarget || targetConfigKeyForPlatform(runtime.target); const targetConfig = getTargetConfig(targetConfigMatrix, configTarget); const effectiveConfig = createEffectiveTargetConfig({ @@ -52,58 +52,3 @@ export async function installStarterPlatform( }, }); } - -async function createPlatformGateway(runtime: RuntimeConfig): Promise { - try { - switch (runtime.target) { - case 'android': - case 'ios': { - const { createCapacitorPlatformGateway } = await import('@mpgd/adapter-capacitor'); - return createCapacitorPlatformGateway({ - target: runtime.target, - appVersion: runtime.appVersion, - buildId: runtime.buildId, - }); - } - - case 'ait': { - const { createAitPlatformGateway, createAitSandboxBridge } = await import( - '@mpgd/adapter-ait' - ); - return createAitPlatformGateway({ - appVersion: runtime.appVersion, - buildId: runtime.buildId, - ...(runtime.debug ? { fallbackBridge: createAitSandboxBridge() } : {}), - }); - } - - case 'reddit': { - const { createDevvitPlatformGateway, createDevvitSandboxBridge } = await import( - '@mpgd/adapter-devvit' - ); - return createDevvitPlatformGateway({ - appVersion: runtime.appVersion, - buildId: runtime.buildId, - ...(shouldUseDevvitSandbox(runtime) ? { fallbackBridge: createDevvitSandboxBridge() } : {}), - }); - } - - default: { - const { createBrowserPlatformGateway } = await import('@mpgd/adapter-browser'); - return createBrowserPlatformGateway(); - } - } - } catch (error) { - throw new Error( - `Failed to initialize platform gateway for target "${runtime.target}": ${formatError(error)}`, - ); - } -} - -function shouldUseDevvitSandbox(runtime: RuntimeConfig): boolean { - return runtime.debug && runtime.buildId === devvitSandboxBuildId; -} - -function formatError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/examples/phaser-starter/vite.config.ts b/examples/phaser-starter/vite.config.ts index ba71f3e79..39a83dae3 100644 --- a/examples/phaser-starter/vite.config.ts +++ b/examples/phaser-starter/vite.config.ts @@ -15,6 +15,11 @@ export default defineConfig(({ mode }) => { const platformTarget = readRuntimePlatformTarget(); const appTarget = process.env.APP_TARGET ?? 'browser'; const isDevvitBuild = appTarget === 'reddit'; + const buildGatewayModule = resolveBuildGatewayModule({ + target: appTarget, + debug: !isProduction, + buildId: process.env.BUILD_ID ?? 'local', + }); return { base: './', @@ -25,7 +30,10 @@ export default defineConfig(({ mode }) => { }), ], resolve: { - alias: createCatalogAliases(), + alias: { + ...createCatalogAliases(), + '#mpgd-platform-gateway': resolve(buildGatewayModule), + }, }, define: { __APP_TARGET__: JSON.stringify(appTarget), @@ -62,6 +70,29 @@ export default defineConfig(({ mode }) => { }; }); +export function resolveBuildGatewayModule(input: { + readonly target: string; + readonly debug: boolean; + readonly buildId: string; +}): string { + switch (input.target) { + case 'android': + return 'src/platform/buildGateways/capacitorAndroid.ts'; + case 'ios': + return 'src/platform/buildGateways/capacitorIos.ts'; + case 'ait': + return input.debug + ? 'src/platform/buildGateways/aitSandbox.ts' + : 'src/platform/buildGateways/ait.ts'; + case 'reddit': + return input.debug && input.buildId === 'devvit-sandbox' + ? 'src/platform/buildGateways/redditSandbox.ts' + : 'src/platform/buildGateways/reddit.ts'; + default: + return 'src/platform/buildGateways/browser.ts'; + } +} + function createCatalogAliases(): Record { const productCatalogFile = readConfiguredPath(process.env.MPGD_PRODUCT_CATALOG_FILE); const adPlacementsFile = readConfiguredPath(process.env.MPGD_AD_PLACEMENTS_FILE); diff --git a/packages/cli/templates/phaser-game/src/platform/buildGatewayModule.ts b/packages/cli/templates/phaser-game/src/platform/buildGatewayModule.ts new file mode 100644 index 000000000..4daea75e8 --- /dev/null +++ b/packages/cli/templates/phaser-game/src/platform/buildGatewayModule.ts @@ -0,0 +1,5 @@ +declare module '#mpgd-platform-gateway' { + export function createBuildGateway( + runtime: import('./runtimeDetector').RuntimeConfig, + ): Promise; +} diff --git a/packages/cli/templates/phaser-game/src/platform/buildGateways/ait.ts b/packages/cli/templates/phaser-game/src/platform/buildGateways/ait.ts new file mode 100644 index 000000000..a8b945ce5 --- /dev/null +++ b/packages/cli/templates/phaser-game/src/platform/buildGateways/ait.ts @@ -0,0 +1,11 @@ +import { createAitPlatformGateway } from '@mpgd/adapter-ait'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createAitPlatformGateway({ + appVersion: runtime.appVersion, + buildId: runtime.buildId, + }); +} diff --git a/packages/cli/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts b/packages/cli/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts new file mode 100644 index 000000000..1ba1ec900 --- /dev/null +++ b/packages/cli/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts @@ -0,0 +1,12 @@ +import { createAitPlatformGateway, createAitSandboxBridge } from '@mpgd/adapter-ait'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createAitPlatformGateway({ + appVersion: runtime.appVersion, + buildId: runtime.buildId, + fallbackBridge: createAitSandboxBridge(), + }); +} diff --git a/packages/cli/templates/phaser-game/src/platform/buildGateways/browser.ts b/packages/cli/templates/phaser-game/src/platform/buildGateways/browser.ts new file mode 100644 index 000000000..19eb70fe8 --- /dev/null +++ b/packages/cli/templates/phaser-game/src/platform/buildGateways/browser.ts @@ -0,0 +1,8 @@ +import { createBrowserPlatformGateway } from '@mpgd/adapter-browser'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(_runtime: RuntimeConfig): Promise { + return createBrowserPlatformGateway(); +} diff --git a/packages/cli/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts b/packages/cli/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts new file mode 100644 index 000000000..3d84eb7ad --- /dev/null +++ b/packages/cli/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts @@ -0,0 +1,12 @@ +import { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createCapacitorPlatformGateway({ + target: 'android', + appVersion: runtime.appVersion, + buildId: runtime.buildId, + }); +} diff --git a/packages/cli/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts b/packages/cli/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts new file mode 100644 index 000000000..e0a6f715e --- /dev/null +++ b/packages/cli/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts @@ -0,0 +1,12 @@ +import { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createCapacitorPlatformGateway({ + target: 'ios', + appVersion: runtime.appVersion, + buildId: runtime.buildId, + }); +} diff --git a/packages/cli/templates/phaser-game/src/platform/buildGateways/reddit.ts b/packages/cli/templates/phaser-game/src/platform/buildGateways/reddit.ts new file mode 100644 index 000000000..c121f881d --- /dev/null +++ b/packages/cli/templates/phaser-game/src/platform/buildGateways/reddit.ts @@ -0,0 +1,11 @@ +import { createDevvitPlatformGateway } from '@mpgd/adapter-devvit'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createDevvitPlatformGateway({ + appVersion: runtime.appVersion, + buildId: runtime.buildId, + }); +} diff --git a/packages/cli/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts b/packages/cli/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts new file mode 100644 index 000000000..7b4b48c5e --- /dev/null +++ b/packages/cli/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts @@ -0,0 +1,15 @@ +import { + createDevvitPlatformGateway, + createDevvitSandboxBridge, +} from '@mpgd/adapter-devvit'; +import type { PlatformGateway } from '@mpgd/platform'; + +import type { RuntimeConfig } from '../runtimeDetector'; + +export async function createBuildGateway(runtime: RuntimeConfig): Promise { + return createDevvitPlatformGateway({ + appVersion: runtime.appVersion, + buildId: runtime.buildId, + fallbackBridge: createDevvitSandboxBridge(), + }); +} diff --git a/packages/cli/templates/phaser-game/src/platform/installPlatform.ts b/packages/cli/templates/phaser-game/src/platform/installPlatform.ts index f1b1f0d88..ab319e420 100644 --- a/packages/cli/templates/phaser-game/src/platform/installPlatform.ts +++ b/packages/cli/templates/phaser-game/src/platform/installPlatform.ts @@ -1,7 +1,8 @@ +import { createBuildGateway } from '#mpgd-platform-gateway'; + import type { AdPlacements, ProductCatalog } from '@mpgd/catalog'; import productCatalogJson from '@mpgd/catalog/catalog.json'; import adPlacementsJson from '@mpgd/catalog/placements.json'; -import type { PlatformGateway } from '@mpgd/platform'; import { createEffectiveTargetConfig, getTargetConfig, @@ -14,7 +15,6 @@ import targetConfigMatrixJson from '@mpgd/target-config/targets.json'; import type { RuntimeConfig } from './runtimeDetector'; -const devvitSandboxBuildId = 'devvit-sandbox'; const targetConfigMatrix = targetConfigMatrixJson as TargetConfigMatrix; const productCatalog = productCatalogJson as ProductCatalog; const adPlacements = adPlacementsJson as AdPlacements; @@ -27,50 +27,7 @@ const adPlacementTypes = new Map( ); export async function installPlatform(runtime: RuntimeConfig): Promise { - let gateway: PlatformGateway; - - switch (runtime.target) { - case 'android': - case 'ios': { - const { createCapacitorPlatformGateway } = await import('@mpgd/adapter-capacitor'); - gateway = createCapacitorPlatformGateway({ - target: runtime.target, - appVersion: runtime.appVersion, - buildId: runtime.buildId, - }); - break; - } - - case 'ait': { - const { createAitPlatformGateway, createAitSandboxBridge } = await import( - '@mpgd/adapter-ait' - ); - gateway = createAitPlatformGateway({ - appVersion: runtime.appVersion, - buildId: runtime.buildId, - ...(runtime.debug ? { fallbackBridge: createAitSandboxBridge() } : {}), - }); - break; - } - - case 'reddit': { - const { createDevvitPlatformGateway, createDevvitSandboxBridge } = await import( - '@mpgd/adapter-devvit' - ); - gateway = createDevvitPlatformGateway({ - appVersion: runtime.appVersion, - buildId: runtime.buildId, - ...(shouldUseDevvitSandbox(runtime) ? { fallbackBridge: createDevvitSandboxBridge() } : {}), - }); - break; - } - - default: { - const { createBrowserPlatformGateway } = await import('@mpgd/adapter-browser'); - gateway = createBrowserPlatformGateway(); - } - } - + const gateway = await createBuildGateway(runtime); const configTarget = runtime.configTarget || targetConfigKeyForPlatform(runtime.target); const targetConfig = getTargetConfig(targetConfigMatrix, configTarget); const effectiveConfig = createEffectiveTargetConfig({ @@ -93,7 +50,3 @@ export async function installPlatform(runtime: RuntimeConfig): Promise { const platformTarget = readRuntimePlatformTarget(); const appTarget = process.env.APP_TARGET ?? 'browser'; const isDevvitBuild = appTarget === 'reddit'; + const buildGatewayModule = resolveBuildGatewayModule({ + target: appTarget, + debug: !isProduction, + buildId: process.env.BUILD_ID ?? 'local', + }); return { base: './', @@ -25,7 +30,10 @@ export default defineConfig(({ mode }) => { }), ], resolve: { - alias: createCatalogAliases(), + alias: { + ...createCatalogAliases(), + '#mpgd-platform-gateway': resolve(buildGatewayModule), + }, }, define: { __APP_TARGET__: JSON.stringify(appTarget), @@ -62,6 +70,29 @@ export default defineConfig(({ mode }) => { }; }); +export function resolveBuildGatewayModule(input: { + readonly target: string; + readonly debug: boolean; + readonly buildId: string; +}): string { + switch (input.target) { + case 'android': + return 'src/platform/buildGateways/capacitorAndroid.ts'; + case 'ios': + return 'src/platform/buildGateways/capacitorIos.ts'; + case 'ait': + return input.debug + ? 'src/platform/buildGateways/aitSandbox.ts' + : 'src/platform/buildGateways/ait.ts'; + case 'reddit': + return input.debug && input.buildId === 'devvit-sandbox' + ? 'src/platform/buildGateways/redditSandbox.ts' + : 'src/platform/buildGateways/reddit.ts'; + default: + return 'src/platform/buildGateways/browser.ts'; + } +} + function createCatalogAliases(): Record { const productCatalogFile = readConfiguredPath(process.env.MPGD_PRODUCT_CATALOG_FILE); const adPlacementsFile = readConfiguredPath(process.env.MPGD_AD_PLACEMENTS_FILE); diff --git a/tools/smoke/verify-target-artifacts.ts b/tools/smoke/verify-target-artifacts.ts index 17634fda4..35e0ecd8b 100644 --- a/tools/smoke/verify-target-artifacts.ts +++ b/tools/smoke/verify-target-artifacts.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync, statSync } from 'node:fs'; +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { basename, dirname, isAbsolute, relative, resolve } from 'node:path'; import { assertReleaseManifest, type ReleaseManifest } from '@mpgd/release-manifest'; @@ -76,6 +76,7 @@ export function verifyTargetArtifacts(targets: readonly string[] = configuredTar if (target === 'microsoft-store') { verifyMicrosoftStorePwaManifest(artifactPath); + verifyMicrosoftStoreBundlePurity(artifactPath); } if (targetConfig.kind === 'devvit-web') { @@ -104,6 +105,59 @@ export function verifyTargetArtifacts(targets: readonly string[] = configuredTar console.log(`Target smoke passed: ${targets.join(', ')}`); } +const forbiddenMicrosoftStoreJavaScriptMarkers = [ + ['AIT bridge is not installed.', 'AIT adapter'], + ['AIT Sandbox Player', 'AIT sandbox identity'], + ['ait-sandbox-reward-', 'AIT sandbox rewarded ad'], + ['CapacitorCookies', 'Capacitor adapter'], + ['DevvitBridgeError', 'Devvit adapter'], + ['EFFECT_REALTIME_SUB', 'Devvit web client'], +] as const; + +function verifyMicrosoftStoreBundlePurity(artifactPath: string): void { + const javascriptFiles = listJavaScriptFiles(artifactPath); + + if (javascriptFiles.length === 0) { + throw new Error('Microsoft Store artifact must contain JavaScript.'); + } + + for (const file of javascriptFiles) { + const source = readFileSync(file, 'utf8'); + const relativeFile = relative(artifactPath, file); + + for (const [marker, owner] of forbiddenMicrosoftStoreJavaScriptMarkers) { + if (source.includes(marker)) { + throw new Error(`Microsoft Store artifact contains ${owner} code in ${relativeFile}.`); + } + } + } +} + +function listJavaScriptFiles(root: string): readonly string[] { + const files: string[] = []; + const pendingDirectories = [root]; + + while (pendingDirectories.length > 0) { + const directory = pendingDirectories.pop(); + + if (directory === undefined) { + throw new Error('JavaScript artifact traversal lost its directory.'); + } + + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const file = resolve(directory, entry.name); + + if (entry.isDirectory()) { + pendingDirectories.push(file); + } else if (entry.isFile() && entry.name.endsWith('.js')) { + files.push(file); + } + } + } + + return files.sort(); +} + function readReleaseEmbeddedTargetConfig( target: string, targetConfig: SmokePlatformTargetConfig, diff --git a/tools/validate-starter-workflow.ts b/tools/validate-starter-workflow.ts index 70cac316a..63b84600d 100644 --- a/tools/validate-starter-workflow.ts +++ b/tools/validate-starter-workflow.ts @@ -157,6 +157,7 @@ validatePhaserTemplateAITPolyfill(); validatePhaserTemplateAITConsoleCli(); validatePhaserTemplateDevvitPostOperations(); validatePhaserTemplateDevvitSurfaces(); +validatePhaserTemplateBuildGateways(); validatePhaserTemplateOrientationPolicy(); if (failures.length > 0) { @@ -165,6 +166,63 @@ if (failures.length > 0) { console.log('Starter workflow validation passed.'); +function validatePhaserTemplateBuildGateways(): void { + const exampleRoot = 'examples/phaser-starter'; + const templateRoot = 'packages/cli/templates/phaser-game'; + const gatewayFiles = [ + 'src/platform/buildGatewayModule.ts', + 'src/platform/buildGateways/browser.ts', + 'src/platform/buildGateways/capacitorAndroid.ts', + 'src/platform/buildGateways/capacitorIos.ts', + 'src/platform/buildGateways/ait.ts', + 'src/platform/buildGateways/aitSandbox.ts', + 'src/platform/buildGateways/reddit.ts', + 'src/platform/buildGateways/redditSandbox.ts', + ] as const; + + for (const relativePath of gatewayFiles) { + const examplePath = `${exampleRoot}/${relativePath}`; + const templatePath = `${templateRoot}/${relativePath}`; + + if (!existsSync(examplePath)) { + failures.push(`${examplePath}: required for target-isolated gateway builds.`); + continue; + } + + if (!existsSync(templatePath)) { + failures.push(`${templatePath}: required for target-isolated gateway builds.`); + continue; + } + + if (readText(examplePath) !== readText(templatePath)) { + failures.push(`${templatePath}: must stay in parity with ${examplePath}.`); + } + } + + for (const root of [exampleRoot, templateRoot]) { + const vitePath = `${root}/vite.config.ts`; + const installPath = `${root}/src/platform/${ + root === exampleRoot ? 'installStarterPlatform.ts' : 'installPlatform.ts' + }`; + + for (const requiredText of [ + "'#mpgd-platform-gateway': resolve(buildGatewayModule)", + 'export function resolveBuildGatewayModule', + "return 'src/platform/buildGateways/browser.ts'", + "'src/platform/buildGateways/aitSandbox.ts'", + "'src/platform/buildGateways/redditSandbox.ts'", + ]) { + assertIncludesText(readText(vitePath), requiredText, `${vitePath}: build gateway isolation.`); + } + + assertIncludesText( + readText(installPath), + "import { createBuildGateway } from '#mpgd-platform-gateway'", + `${installPath}: build-selected gateway.`, + ); + } +} + function validateSkillFrontmatter(path: string): void { const content = readText(path); const match = /^---\r?\n(?[\s\S]*?)\r?\n---/.exec(content);