// optional, best-effort access to the INTERNAL demo-app registry // (`./demo-app-registry.ts`). that file is repo-internal dev tooling: it pins // real demo apps to fixed ports (e.g. 3pc on 8081) and is consumed only by // `bun run demo` + the in-repo electron/vite discovery paths, which run from // the typescript source where the registry is always present. // // the PUBLISHED `sootsim` npm package must know nothing about those internal // apps — shipping the registry once matched a brand-new user's `expo start` on // the default port 8081 against the 3pc `one` entry and crashed their metro // (repo-agnostic-pipeline violation). so the registry is NOT in the package // `files` list, and the `import()` below is resolved to an absolute path at // runtime: it finds the source registry in-repo and nothing (empty list) on a // published install. no bundler can see through that, so it never inlines. // // one path, no env toggle: every consumer goes through this loader; whether the // registry resolves is purely a function of whether the file exists on disk. import { existsSync } from 'node:fs' import { basename, dirname, join } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' // this module is emitted at three different depths — the source at // packages/sootsim/scripts/, the built CLI at packages/sootsim/dist-cli/chunks/, // and the electron main bundle at packages/sootsim-engine/dist-electron/ — so a // relative './demo-app-registry.ts' is only correct from the source copy. from // the other two it points at a file that does not exist, the catch below // swallows that, and every demo app then boots with no runtimeConfig at all // (mattermost stops on the server-select screen because AutoSelectServerUrl // never reaches it). the one description that holds from every emit site is the // ancestor that holds the registry at its repo path, which in-repo is the // monorepo root. const REGISTRY_REPO_PATH = 'packages/sootsim/scripts/demo-app-registry.ts' // the walk must never leave the installed package. `sootsim` publishes this // loader's source, so on a published install it runs from // node_modules/sootsim/scripts/ (or dist-cli/chunks/) with the consumer's whole // filesystem above it — and consumers include this repo's own templates and // examples, which install sootsim while sitting inside the monorepo. an // unbounded walk from there reaches the real registry and hands a user's // `expo start` on port 8081 the internal 3pc entry, which is the exact incident // the header describes. a node_modules ancestor means "installed as a // dependency", so the search stops there and the registry stays absent. const MAX_ANCESTOR_STEPS = 8 /** absolute path to the internal registry, or null when this copy is running * outside the monorepo (a published install) and must not have one. */ export function findInternalDemoRegistry(startDir: string): string | null { let dir = startDir for (let step = 0; step < MAX_ANCESTOR_STEPS; step++) { if (basename(dir) === 'node_modules') return null const candidate = join(dir, REGISTRY_REPO_PATH) if (existsSync(candidate)) return candidate const parent = dirname(dir) if (parent === dir) return null dir = parent } return null } export interface OptionalDemoApp { name: string label: string dir: string preferredPort: number framework: 'expo' | 'one' | 'rock' runtimeConfig?: import('../src/config.ts').RNXConfig } let cached: OptionalDemoApp[] | null = null /** load the internal demo registry if it's present on disk (in-repo only). * returns [] on a published install where the registry was not shipped. */ export async function loadOptionalDemoApps(): Promise { if (cached) return cached const registryPath = findInternalDemoRegistry(dirname(fileURLToPath(import.meta.url))) if (!registryPath) { cached = [] return cached } try { const mod = (await import(/* @vite-ignore */ pathToFileURL(registryPath).href)) as { APPS?: OptionalDemoApp[] } cached = Array.isArray(mod.APPS) ? mod.APPS : [] } catch { cached = [] } return cached } /** synchronous view of the registry — empty until `loadOptionalDemoApps()` has * resolved at least once. every caller that builds a result from the registry * awaits the load first. */ export function optionalDemoAppsSync(): OptionalDemoApp[] { return cached ?? [] } /** replace the registry with a fixed list, and restore it on the returned * handle. what the real registry holds depends on which third-party app * checkouts and credential files happen to exist on the machine, so a test * that reads it asserts on the developer's home directory rather than on the * code. it passes on the box that has them and fails everywhere else. */ export function __withOptionalDemoAppsForTests(apps: OptionalDemoApp[]): { restore: () => void } { const previous = cached cached = apps return { restore: () => { cached = previous }, } }