@absolutejs/absolute 0.20.0-beta.167 → 0.20.0-beta.169
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/CHANGELOG.md +18 -0
- package/README.md +81 -0
- package/changelog.json +35 -0
- package/dist/angular/browser.js +3 -3
- package/dist/angular/browser.js.map +3 -3
- package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/angular/index.js +211 -197
- package/dist/angular/index.js.map +8 -8
- package/dist/angular/server.js +211 -197
- package/dist/angular/server.js.map +8 -8
- package/dist/build.js +1324 -998
- package/dist/build.js.map +12 -8
- package/dist/cli/{add-kwyqxccz.js → add-7azr0c83.js} +5 -5
- package/dist/cli/{androidEmulatorController-3zn6a4j3.js → androidEmulatorController-68jjdxmx.js} +3 -3
- package/dist/cli/{androidNativeWatcher-wz7vhnkr.js → androidNativeWatcher-x032x9yr.js} +3 -3
- package/dist/cli/{api-m3n8xbcv.js → api-mshjr4ah.js} +1 -1
- package/dist/cli/{compile-30m09382.js → compile-4jnfxts9.js} +2 -2
- package/dist/cli/config/server.js +129 -115
- package/dist/cli/{dev-ny2vah78.js → dev-9tk5hn2e.js} +9 -9
- package/dist/cli/{emulatorDoctor-1m4rr9y3.js → emulatorDoctor-p3yt7140.js} +2 -2
- package/dist/cli/{emulatorInstaller-70v711bj.js → emulatorInstaller-4pm2kx9w.js} +3 -3
- package/dist/cli/{expoDevController-hen9sra5.js → expoDevController-0vj8dvkh.js} +3 -3
- package/dist/cli/{generate-vx6tz1y2.js → generate-v75p2fct.js} +4 -4
- package/dist/cli/{htmx-dym03fq5.js → htmx-427swtdz.js} +2 -2
- package/dist/cli/{index-j9yrhw7a.js → index-066qhc6n.js} +1 -1
- package/dist/cli/{index-x1swv7rw.js → index-3vmc30gf.js} +1 -1
- package/dist/cli/{index-g8489j4d.js → index-a4ws2jwd.js} +1 -1
- package/dist/cli/{index-xj68ep9n.js → index-b7hjgknf.js} +2 -2
- package/dist/cli/{index-vkba3289.js → index-bhbs6d5z.js} +2 -2
- package/dist/cli/{index-v4cg4dvm.js → index-exfkpjja.js} +4 -3
- package/dist/cli/{index-vy8zejts.js → index-eyzwc7gz.js} +1 -1
- package/dist/cli/{index-d3br7c47.js → index-k3y8cb6c.js} +1 -1
- package/dist/cli/{index-52a2csjm.js → index-ppphkyps.js} +1 -1
- package/dist/cli/{index-bvwkwgjw.js → index-s3xvccq3.js} +1 -1
- package/dist/cli/{index-6ps18fvr.js → index-t8cf0zj9.js} +2 -2
- package/dist/cli/{index-wwnnfhm6.js → index-w09fzs9f.js} +1 -1
- package/dist/cli/{index-px7fqsfk.js → index-zd3cna19.js} +1 -1
- package/dist/cli/index.js +16 -16
- package/dist/cli/{info-9ffx5h3h.js → info-mn125212.js} +1 -1
- package/dist/cli/{iosNativeWatcher-ntzec3hx.js → iosNativeWatcher-0nm6qad5.js} +4 -4
- package/dist/cli/{iosSimulatorController-hjzy6pq6.js → iosSimulatorController-hza2c3rv.js} +4 -4
- package/dist/cli/{islands-rq6wx0kh.js → islands-8sgky9gh.js} +1 -1
- package/dist/cli/{mobile-41sev6v0.js → mobile-hcxjr2gf.js} +10 -10
- package/dist/cli/{ps-0tryaye0.js → ps-ezmzyscn.js} +1 -1
- package/dist/cli/{psTui-0nx63pmr.js → psTui-3x3ykjve.js} +1 -1
- package/dist/cli/{remoteMacProtocol-2ej6fbnj.js → remoteMacProtocol-g09myqt5.js} +4 -4
- package/dist/cli/{remove-qgme4h2s.js → remove-1ycjx85k.js} +4 -4
- package/dist/cli/{start-0ebxkwga.js → start-mbaejxhm.js} +3 -3
- package/dist/cli/{typecheck-7yhekrc0.js → typecheck-dr068pmh.js} +209 -43
- package/dist/cli/{workspace-atcxzyee.js → workspace-1sycp9g1.js} +7 -4
- package/dist/client/index.js +8 -3
- package/dist/client/index.js.map +4 -4
- package/dist/index.js +1402 -1075
- package/dist/index.js.map +14 -10
- package/dist/islands/browser.js +3 -3
- package/dist/islands/browser.js.map +3 -3
- package/dist/islands/index.js +3 -3
- package/dist/islands/index.js.map +3 -3
- package/dist/mobile/index.js.map +1 -1
- package/dist/react/browser.js +3 -3
- package/dist/react/browser.js.map +3 -3
- package/dist/react/index.js +231 -217
- package/dist/react/index.js.map +9 -9
- package/dist/react/server.js +231 -191
- package/dist/react/server.js.map +9 -8
- package/dist/src/build/islandBootstrap.d.ts +22 -0
- package/dist/src/build/missingClientFrameworks.d.ts +29 -0
- package/dist/src/build/svelteDeclarations.d.ts +27 -0
- package/dist/src/cli/utils.d.ts +5 -1
- package/dist/src/client/islandResolver.d.ts +3 -1
- package/dist/src/core/build.d.ts +1 -1
- package/dist/src/core/islandManifest.d.ts +10 -0
- package/dist/src/core/islandPageContext.d.ts +2 -0
- package/dist/src/dev/svelteDeclarationRefresher.d.ts +6 -0
- package/dist/svelte/browser.js +3 -3
- package/dist/svelte/browser.js.map +3 -3
- package/dist/svelte/index.js +211 -197
- package/dist/svelte/index.js.map +8 -8
- package/dist/svelte/server.js +211 -171
- package/dist/svelte/server.js.map +8 -7
- package/dist/vue/browser.js +3 -3
- package/dist/vue/browser.js.map +3 -3
- package/dist/vue/index.js +231 -217
- package/dist/vue/index.js.map +9 -9
- package/dist/vue/server.js +231 -191
- package/dist/vue/server.js.map +9 -8
- package/package.json +6 -1
- /package/dist/cli/{index-e6ysp900.js → index-z794p5qg.js} +0 -0
package/dist/mobile/index.js.map
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"import { createHash } from 'node:crypto';\nimport {\n\taccess,\n\tcopyFile,\n\tmkdir,\n\treadFile,\n\trename,\n\trm,\n\twriteFile\n} from 'node:fs/promises';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport type { NormalizedAbsoluteMobileConfig } from './config';\n\nexport const ABSOLUTE_MOBILE_BRANDING_FORMAT = 1 as const;\nexport const ABSOLUTE_CAPACITOR_ASSETS_VERSION = '3.0.5' as const;\nexport const ABSOLUTE_CAPACITOR_ASSETS_SPEC =\n\t`@capacitor/assets@${ABSOLUTE_CAPACITOR_ASSETS_VERSION}` as const;\n\nconst MAX_SOURCE_BYTES = 32 * 1024 * 1024;\nconst ICON_SIZE = 1024;\nconst SPLASH_SIZE = 2732;\nconst ANDROID_ADAPTIVE_SIZES = [\n\t['ldpi', 81],\n\t['mdpi', 108],\n\t['hdpi', 162],\n\t['xhdpi', 216],\n\t['xxhdpi', 324],\n\t['xxxhdpi', 432]\n] as const;\n\ntype Branding = NonNullable<NormalizedAbsoluteMobileConfig['branding']>;\n\nexport type AbsoluteMobileBrandingSource = {\n\tbytes: number;\n\theight: number;\n\tkind:\n\t\t| 'android-background'\n\t\t| 'android-foreground'\n\t\t| 'android-monochrome'\n\t\t| 'icon'\n\t\t| 'ios-dark'\n\t\t| 'ios-tinted'\n\t\t| 'splash-dark'\n\t\t| 'splash-light';\n\tpath: string;\n\tsha256: string;\n\twidth: number;\n};\n\nexport type AbsoluteMobileBrandingManifest = {\n\tengine: 'capacitor' | 'expo';\n\tfingerprint: string;\n\tformat: typeof ABSOLUTE_MOBILE_BRANDING_FORMAT;\n\tplatforms: ('android' | 'ios')[];\n\tsources: AbsoluteMobileBrandingSource[];\n};\n\nexport type AbsoluteMobileBrandingInspection = {\n\tconfigured: boolean;\n\tdetail: string;\n\tmanifestPath: string;\n\tready: boolean;\n\tstatus: 'absent' | 'invalid' | 'ready' | 'stale';\n};\n\nexport type AbsoluteMobileBrandingRunner = (\n\tcommand: string[],\n\toptions: { cwd: string }\n) => Promise<void>;\n\nconst exists = async (path: string) => {\n\ttry {\n\t\tawait access(path);\n\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\nconst portableRelative = (root: string, path: string) =>\n\trelative(root, path).replaceAll('\\\\', '/');\n\nexport const absoluteMobileBrandingManifestPath = (projectRoot: string) =>\n\tjoin(\n\t\tresolve(projectRoot),\n\t\t'.absolutejs',\n\t\t'mobile',\n\t\t'branding',\n\t\t'manifest.json'\n\t);\n\nconst brandingEntries = (branding: Branding) =>\n\t[\n\t\t{ kind: 'icon' as const, path: branding.icon, size: ICON_SIZE },\n\t\tbranding.android.foreground\n\t\t\t? {\n\t\t\t\t\tkind: 'android-foreground' as const,\n\t\t\t\t\tpath: branding.android.foreground,\n\t\t\t\t\tsize: ICON_SIZE\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tbranding.android.backgroundImage\n\t\t\t? {\n\t\t\t\t\tkind: 'android-background' as const,\n\t\t\t\t\tpath: branding.android.backgroundImage,\n\t\t\t\t\tsize: ICON_SIZE\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tbranding.android.monochrome\n\t\t\t? {\n\t\t\t\t\tkind: 'android-monochrome' as const,\n\t\t\t\t\tpath: branding.android.monochrome,\n\t\t\t\t\tsize: ICON_SIZE\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tbranding.ios.darkIcon\n\t\t\t? {\n\t\t\t\t\tkind: 'ios-dark' as const,\n\t\t\t\t\tpath: branding.ios.darkIcon,\n\t\t\t\t\tsize: ICON_SIZE\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tbranding.ios.tintedIcon\n\t\t\t? {\n\t\t\t\t\tkind: 'ios-tinted' as const,\n\t\t\t\t\tpath: branding.ios.tintedIcon,\n\t\t\t\t\tsize: ICON_SIZE\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tbranding.splash.image\n\t\t\t? {\n\t\t\t\t\tkind: 'splash-light' as const,\n\t\t\t\t\tpath: branding.splash.image,\n\t\t\t\t\tsize: SPLASH_SIZE\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tbranding.splash.darkImage\n\t\t\t? {\n\t\t\t\t\tkind: 'splash-dark' as const,\n\t\t\t\t\tpath: branding.splash.darkImage,\n\t\t\t\t\tsize: SPLASH_SIZE\n\t\t\t\t}\n\t\t\t: undefined\n\t].filter(\n\t\t(entry): entry is NonNullable<typeof entry> => entry !== undefined\n\t);\n\nconst inspectSource = async (\n\tprojectRoot: string,\n\tentry: ReturnType<typeof brandingEntries>[number]\n): Promise<AbsoluteMobileBrandingSource> => {\n\tconst buffer = await readFile(entry.path);\n\tif (buffer.byteLength > MAX_SOURCE_BYTES)\n\t\tthrow new TypeError(\n\t\t\t`mobile.branding ${entry.kind} exceeds the 32 MiB source limit.`\n\t\t);\n\tlet metadata: Bun.Image.Metadata;\n\ttry {\n\t\tmetadata = await new Bun.Image(buffer, {\n\t\t\tmaxPixels: SPLASH_SIZE * SPLASH_SIZE * 4\n\t\t}).metadata();\n\t} catch (error) {\n\t\tthrow new TypeError(\n\t\t\t`mobile.branding ${entry.kind} must be a readable PNG image.`,\n\t\t\t{ cause: error }\n\t\t);\n\t}\n\tif (metadata.format !== 'png')\n\t\tthrow new TypeError(`mobile.branding ${entry.kind} must be PNG.`);\n\tif (\n\t\tmetadata.width !== metadata.height ||\n\t\tmetadata.width < entry.size ||\n\t\tmetadata.height < entry.size\n\t)\n\t\tthrow new TypeError(\n\t\t\t`mobile.branding ${entry.kind} must be square and at least ${entry.size}x${entry.size}.`\n\t\t);\n\n\treturn {\n\t\tbytes: buffer.byteLength,\n\t\theight: metadata.height,\n\t\tkind: entry.kind,\n\t\tpath: portableRelative(projectRoot, entry.path),\n\t\tsha256: createHash('sha256').update(buffer).digest('hex'),\n\t\twidth: metadata.width\n\t};\n};\n\nconst manifestFingerprint = (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tsources: AbsoluteMobileBrandingSource[]\n) => {\n\tconst { branding } = config;\n\tif (!branding) throw new TypeError('mobile.branding is not configured.');\n\n\treturn createHash('sha256')\n\t\t.update(\n\t\t\tJSON.stringify({\n\t\t\t\tandroidBackgroundColor: branding.android.backgroundColor,\n\t\t\t\tengine: config.engine,\n\t\t\t\tformat: ABSOLUTE_MOBILE_BRANDING_FORMAT,\n\t\t\t\tplatforms: config.platforms,\n\t\t\t\tsources: sources.map(({ kind, sha256 }) => ({ kind, sha256 })),\n\t\t\t\tsplashBackgroundColor: branding.splash.backgroundColor,\n\t\t\t\tsplashDarkBackgroundColor: branding.splash.darkBackgroundColor,\n\t\t\t\tsplashLogoScale: branding.splash.logoScale\n\t\t\t})\n\t\t)\n\t\t.digest('hex');\n};\n\nexport const createAbsoluteMobileBrandingManifest = async (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tprojectRoot: string\n) => {\n\tif (!config.branding)\n\t\tthrow new TypeError(\n\t\t\t'absolute.config.ts must define mobile.branding before generating mobile assets.'\n\t\t);\n\tconst sources = await Promise.all(\n\t\tbrandingEntries(config.branding).map((entry) =>\n\t\t\tinspectSource(projectRoot, entry)\n\t\t)\n\t);\n\n\treturn {\n\t\tengine: config.engine,\n\t\tfingerprint: manifestFingerprint(config, sources),\n\t\tformat: ABSOLUTE_MOBILE_BRANDING_FORMAT,\n\t\tplatforms: [...config.platforms],\n\t\tsources\n\t};\n};\n\nconst writeManifest = async (\n\tprojectRoot: string,\n\tmanifest: AbsoluteMobileBrandingManifest\n) => {\n\tconst destination = absoluteMobileBrandingManifestPath(projectRoot);\n\tawait mkdir(dirname(destination), { recursive: true });\n\tconst temporary = `${destination}.${crypto.randomUUID()}.tmp`;\n\tawait writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\\n`, {\n\t\tflag: 'wx'\n\t});\n\tawait rename(temporary, destination);\n\n\treturn destination;\n};\n\nconst copyManaged = async (source: string, destination: string) => {\n\tawait mkdir(dirname(destination), { recursive: true });\n\tawait copyFile(source, destination);\n};\n\nexport const absoluteExpoBrandingConfig = (\n\tconfig: NormalizedAbsoluteMobileConfig\n) => {\n\tconst { branding } = config;\n\tif (!branding) return undefined;\n\tconst root = './assets/absolute-branding';\n\tconst iosIcon =\n\t\tbranding.ios.darkIcon || branding.ios.tintedIcon\n\t\t\t? {\n\t\t\t\t\t...(branding.ios.darkIcon\n\t\t\t\t\t\t? { dark: `${root}/ios-dark.png` }\n\t\t\t\t\t\t: {}),\n\t\t\t\t\tlight: `${root}/icon.png`,\n\t\t\t\t\t...(branding.ios.tintedIcon\n\t\t\t\t\t\t? { tinted: `${root}/ios-tinted.png` }\n\t\t\t\t\t\t: {})\n\t\t\t\t}\n\t\t\t: `${root}/icon.png`;\n\n\treturn {\n\t\tandroid: {\n\t\t\tadaptiveIcon: {\n\t\t\t\tbackgroundColor: branding.android.backgroundColor,\n\t\t\t\t...(branding.android.backgroundImage\n\t\t\t\t\t? { backgroundImage: `${root}/android-background.png` }\n\t\t\t\t\t: {}),\n\t\t\t\tforegroundImage: branding.android.foreground\n\t\t\t\t\t? `${root}/android-foreground.png`\n\t\t\t\t\t: `${root}/icon.png`,\n\t\t\t\t...(branding.android.monochrome\n\t\t\t\t\t? { monochromeImage: `${root}/android-monochrome.png` }\n\t\t\t\t\t: {})\n\t\t\t},\n\t\t\ticon: `${root}/icon.png`\n\t\t},\n\t\ticon: `${root}/icon.png`,\n\t\tios: { icon: iosIcon },\n\t\tsplashPlugin: [\n\t\t\t'expo-splash-screen',\n\t\t\t{\n\t\t\t\tbackgroundColor: branding.splash.backgroundColor,\n\t\t\t\tdark: {\n\t\t\t\t\tbackgroundColor: branding.splash.darkBackgroundColor,\n\t\t\t\t\timage: branding.splash.darkImage\n\t\t\t\t\t\t? `${root}/splash-dark.png`\n\t\t\t\t\t\t: `${root}/icon.png`\n\t\t\t\t},\n\t\t\t\timage: branding.splash.image\n\t\t\t\t\t? `${root}/splash-light.png`\n\t\t\t\t\t: `${root}/icon.png`,\n\t\t\t\timageWidth: Math.round(1024 * branding.splash.logoScale)\n\t\t\t}\n\t\t]\n\t};\n};\n\nexport const writeAbsoluteExpoBrandingInputs = async (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tprojectRoot: string\n) => {\n\tconst { branding } = config;\n\tif (!branding) return undefined;\n\tconst manifest = await createAbsoluteMobileBrandingManifest(\n\t\tconfig,\n\t\tprojectRoot\n\t);\n\tconst destination = join(\n\t\tconfig.nativeProjectDirectory,\n\t\t'assets',\n\t\t'absolute-branding'\n\t);\n\tawait rm(destination, { force: true, recursive: true });\n\tawait mkdir(destination, { recursive: true });\n\tawait copyManaged(branding.icon, join(destination, 'icon.png'));\n\tconst copies = [\n\t\t[branding.android.backgroundImage, 'android-background.png'],\n\t\t[branding.android.foreground, 'android-foreground.png'],\n\t\t[branding.android.monochrome, 'android-monochrome.png'],\n\t\t[branding.ios.darkIcon, 'ios-dark.png'],\n\t\t[branding.ios.tintedIcon, 'ios-tinted.png'],\n\t\t[branding.splash.image, 'splash-light.png'],\n\t\t[branding.splash.darkImage, 'splash-dark.png']\n\t] as const;\n\tawait Promise.all(\n\t\tcopies.map(([source, name]) =>\n\t\t\tsource ? copyManaged(source, join(destination, name)) : undefined\n\t\t)\n\t);\n\tawait writeManifest(projectRoot, manifest);\n\n\treturn { manifest, path: destination };\n};\n\nconst stageCapacitorInputs = async (\n\tbranding: Branding,\n\tprojectRoot: string\n) => {\n\tconst destination = join(\n\t\tprojectRoot,\n\t\t'.absolutejs',\n\t\t'mobile',\n\t\t'branding',\n\t\t'input'\n\t);\n\tawait rm(destination, { force: true, recursive: true });\n\tawait mkdir(destination, { recursive: true });\n\tawait copyManaged(branding.icon, join(destination, 'logo.png'));\n\tif (branding.splash.image)\n\t\tawait copyManaged(\n\t\t\tbranding.splash.image,\n\t\t\tjoin(destination, 'splash.png')\n\t\t);\n\tif (branding.splash.darkImage)\n\t\tawait copyManaged(\n\t\t\tbranding.splash.darkImage,\n\t\t\tjoin(destination, 'splash-dark.png')\n\t\t);\n\n\treturn destination;\n};\n\nconst defaultRunner: AbsoluteMobileBrandingRunner = async (\n\tcommand,\n\toptions\n) => {\n\tconst child = Bun.spawn(command, {\n\t\tcwd: options.cwd,\n\t\tstderr: 'pipe',\n\t\tstdin: 'inherit',\n\t\tstdout: 'pipe'\n\t});\n\tconst [exitCode, stdout, stderr] = await Promise.all([\n\t\tchild.exited,\n\t\tnew Response(child.stdout).text(),\n\t\tnew Response(child.stderr).text()\n\t]);\n\tif (stdout) process.stdout.write(stdout);\n\tif (stderr) process.stderr.write(stderr);\n\tif (exitCode !== 0)\n\t\tthrow new TypeError(\n\t\t\t`Capacitor asset generation exited with status ${exitCode}.`\n\t\t);\n\t// The pinned upstream CLI catches generation errors and exits zero.\n\t// Existing template artwork must not make those failures look successful.\n\tif (\n\t\t/Unable to generate assets|Unable to load source image|Asset directory not found|No assets found|No platforms found|skipping (?:iOS|android) generation/iu.test(\n\t\t\t`${stdout}\\n${stderr}`\n\t\t)\n\t)\n\t\tthrow new TypeError(\n\t\t\t'Capacitor asset generation reported an incomplete projection.'\n\t\t);\n};\n\nconst resizePng = async (source: string, destination: string, size: number) => {\n\tconst output = await new Bun.Image(await readFile(source))\n\t\t.resize(size, size)\n\t\t.png({ compressionLevel: 9 })\n\t\t.toBuffer();\n\tawait mkdir(dirname(destination), { recursive: true });\n\tawait writeFile(destination, output);\n};\n\nconst applyAndroidLayers = async (\n\tbranding: Branding,\n\tnativeDirectory: string\n) => {\n\tconst resources = join(\n\t\tnativeDirectory,\n\t\t'android',\n\t\t'app',\n\t\t'src',\n\t\t'main',\n\t\t'res'\n\t);\n\tawait Promise.all(\n\t\tANDROID_ADAPTIVE_SIZES.flatMap(([density, size]) => [\n\t\t\t...(branding.android.foreground\n\t\t\t\t? [\n\t\t\t\t\t\tresizePng(\n\t\t\t\t\t\t\tbranding.android.foreground,\n\t\t\t\t\t\t\tjoin(\n\t\t\t\t\t\t\t\tresources,\n\t\t\t\t\t\t\t\t`mipmap-${density}`,\n\t\t\t\t\t\t\t\t'ic_launcher_foreground.png'\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tsize\n\t\t\t\t\t\t)\n\t\t\t\t\t]\n\t\t\t\t: []),\n\t\t\t...(branding.android.backgroundImage\n\t\t\t\t? [\n\t\t\t\t\t\tresizePng(\n\t\t\t\t\t\t\tbranding.android.backgroundImage,\n\t\t\t\t\t\t\tjoin(\n\t\t\t\t\t\t\t\tresources,\n\t\t\t\t\t\t\t\t`mipmap-${density}`,\n\t\t\t\t\t\t\t\t'ic_launcher_background.png'\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tsize\n\t\t\t\t\t\t)\n\t\t\t\t\t]\n\t\t\t\t: []),\n\t\t\t...(branding.android.monochrome\n\t\t\t\t? [\n\t\t\t\t\t\tresizePng(\n\t\t\t\t\t\t\tbranding.android.monochrome,\n\t\t\t\t\t\t\tjoin(\n\t\t\t\t\t\t\t\tresources,\n\t\t\t\t\t\t\t\t`mipmap-${density}`,\n\t\t\t\t\t\t\t\t'ic_launcher_monochrome.png'\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tsize\n\t\t\t\t\t\t)\n\t\t\t\t\t]\n\t\t\t\t: [])\n\t\t])\n\t);\n\tif (!branding.android.monochrome) return;\n\tconst iconXml = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\\n <background android:drawable=\"@mipmap/ic_launcher_background\" />\\n <foreground android:drawable=\"@mipmap/ic_launcher_foreground\" />\\n <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\" />\\n</adaptive-icon>\\n`;\n\tawait Promise.all(\n\t\t['ic_launcher.xml', 'ic_launcher_round.xml'].map((name) =>\n\t\t\twriteFile(join(resources, 'mipmap-anydpi-v26', name), iconXml)\n\t\t)\n\t);\n};\n\nconst applyIosAppearances = async (\n\tbranding: Branding,\n\tnativeDirectory: string\n) => {\n\tif (!branding.ios.darkIcon && !branding.ios.tintedIcon) return;\n\tconst assetSet = join(\n\t\tnativeDirectory,\n\t\t'ios',\n\t\t'App',\n\t\t'App',\n\t\t'Assets.xcassets',\n\t\t'AppIcon.appiconset'\n\t);\n\tconst contentsPath = join(assetSet, 'Contents.json');\n\tconst contents: unknown = JSON.parse(await readFile(contentsPath, 'utf8'));\n\tconst images =\n\t\ttypeof contents === 'object' && contents !== null\n\t\t\t? Reflect.get(contents, 'images')\n\t\t\t: undefined;\n\tif (\n\t\ttypeof contents !== 'object' ||\n\t\tcontents === null ||\n\t\t!Array.isArray(images)\n\t)\n\t\tthrow new TypeError('Generated iOS AppIcon Contents.json is invalid.');\n\tconst appearanceSources: ['dark' | 'tinted', string | undefined][] = [\n\t\t['dark', branding.ios.darkIcon],\n\t\t['tinted', branding.ios.tintedIcon]\n\t];\n\tconst generated = await Promise.all(\n\t\tappearanceSources.map(async ([appearance, source]) => {\n\t\t\tif (!source) return undefined;\n\t\t\tconst filename = `AppIcon-1024-${appearance}.png`;\n\t\t\tawait resizePng(source, join(assetSet, filename), ICON_SIZE);\n\n\t\t\treturn {\n\t\t\t\tappearances: [{ appearance: 'luminosity', value: appearance }],\n\t\t\t\tfilename,\n\t\t\t\tidiom: 'universal',\n\t\t\t\tplatform: 'ios',\n\t\t\t\tsize: '1024x1024'\n\t\t\t};\n\t\t})\n\t);\n\tconst retained = images.filter((entry) => {\n\t\tif (typeof entry !== 'object' || entry === null) return true;\n\t\tconst filename = Reflect.get(entry, 'filename');\n\n\t\treturn (\n\t\t\tfilename !== 'AppIcon-1024-dark.png' &&\n\t\t\tfilename !== 'AppIcon-1024-tinted.png'\n\t\t);\n\t});\n\tReflect.set(contents, 'images', [\n\t\t...retained,\n\t\t...generated.filter((entry) => entry !== undefined)\n\t]);\n\tawait writeFile(contentsPath, `${JSON.stringify(contents, null, 2)}\\n`);\n};\n\nconst nativeBrandingSentinels = (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tplatforms: readonly ('android' | 'ios')[]\n) =>\n\tplatforms.map((platform) =>\n\t\tplatform === 'android'\n\t\t\t? join(\n\t\t\t\t\tconfig.nativeProjectDirectory,\n\t\t\t\t\t'android',\n\t\t\t\t\t'app',\n\t\t\t\t\t'src',\n\t\t\t\t\t'main',\n\t\t\t\t\t'res',\n\t\t\t\t\t'mipmap-xxxhdpi',\n\t\t\t\t\t'ic_launcher.png'\n\t\t\t\t)\n\t\t\t: join(\n\t\t\t\t\tconfig.nativeProjectDirectory,\n\t\t\t\t\t'ios',\n\t\t\t\t\t'App',\n\t\t\t\t\t'App',\n\t\t\t\t\t'Assets.xcassets',\n\t\t\t\t\t'AppIcon.appiconset',\n\t\t\t\t\t'AppIcon-512@2x.png'\n\t\t\t\t)\n\t);\n\nconst requireGeneratedBranding = async (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tplatforms: readonly ('android' | 'ios')[]\n) => {\n\tconst missing = (\n\t\tawait Promise.all(\n\t\t\tnativeBrandingSentinels(config, platforms).map(async (path) => ({\n\t\t\t\tpath,\n\t\t\t\tpresent: await exists(path)\n\t\t\t}))\n\t\t)\n\t).filter(({ present }) => !present);\n\tif (missing.length > 0)\n\t\tthrow new TypeError(\n\t\t\t`Capacitor asset generation did not produce ${missing.map(({ path }) => path).join(', ')}.`\n\t\t);\n};\n\nconst reusableManifestPlatforms = (value: unknown, fingerprint: string) => {\n\tif (\n\t\ttypeof value !== 'object' ||\n\t\tvalue === null ||\n\t\tReflect.get(value, 'format') !== ABSOLUTE_MOBILE_BRANDING_FORMAT ||\n\t\tReflect.get(value, 'fingerprint') !== fingerprint\n\t)\n\t\treturn [];\n\tconst platforms = Reflect.get(value, 'platforms');\n\tif (!Array.isArray(platforms)) return [];\n\n\treturn platforms.flatMap((platform) =>\n\t\tplatform === 'android' || platform === 'ios' ? [platform] : []\n\t);\n};\n\nconst completedManifestPlatforms = async (\n\tmanifest: AbsoluteMobileBrandingManifest,\n\tmanifestPath: string,\n\tgenerated: readonly ('android' | 'ios')[]\n) => {\n\tlet previousPlatforms: ('android' | 'ios')[] = [];\n\ttry {\n\t\tconst previous: unknown = JSON.parse(\n\t\t\tawait readFile(manifestPath, 'utf8')\n\t\t);\n\t\tpreviousPlatforms = reusableManifestPlatforms(\n\t\t\tprevious,\n\t\t\tmanifest.fingerprint\n\t\t);\n\t} catch {\n\t\t// A missing or invalid prior manifest cannot contribute completed work.\n\t}\n\tconst completed = new Set([...generated, ...previousPlatforms]);\n\n\treturn manifest.platforms.filter((platform) => completed.has(platform));\n};\n\nexport const generateAbsoluteCapacitorBranding = async (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tprojectRoot: string,\n\toptions: AbsoluteMobileBrandingGenerationOptions = {}\n) => {\n\tconst { branding } = config;\n\tif (!branding)\n\t\tthrow new TypeError(\n\t\t\t'absolute.config.ts must define mobile.branding before generating mobile assets.'\n\t\t);\n\tif (config.engine !== 'capacitor')\n\t\tthrow new TypeError(\n\t\t\t'Capacitor branding requires mobile.engine: capacitor.'\n\t\t);\n\tconst manifest = await createAbsoluteMobileBrandingManifest(\n\t\tconfig,\n\t\tprojectRoot\n\t);\n\tconst input = await stageCapacitorInputs(branding, projectRoot);\n\tconst platforms = options.platforms ?? config.platforms;\n\tconst command = [\n\t\t'bun',\n\t\t'x',\n\t\t'--no-install',\n\t\t'capacitor-assets',\n\t\t'generate',\n\t\t...platforms.map((platform) => `--${platform}`),\n\t\t'--assetPath',\n\t\tportableRelative(projectRoot, input),\n\t\t'--androidProject',\n\t\tportableRelative(\n\t\t\tprojectRoot,\n\t\t\tjoin(config.nativeProjectDirectory, 'android')\n\t\t),\n\t\t'--iosProject',\n\t\tportableRelative(\n\t\t\tprojectRoot,\n\t\t\tjoin(config.nativeProjectDirectory, 'ios', 'App')\n\t\t),\n\t\t'--iconBackgroundColor',\n\t\tbranding.android.backgroundColor,\n\t\t'--splashBackgroundColor',\n\t\tbranding.splash.backgroundColor,\n\t\t'--splashBackgroundColorDark',\n\t\tbranding.splash.darkBackgroundColor,\n\t\t'--logoSplashScale',\n\t\tString(branding.splash.logoScale)\n\t];\n\tawait (options.runner ?? defaultRunner)(command, { cwd: projectRoot });\n\tawait requireGeneratedBranding(config, platforms);\n\tif (platforms.includes('android'))\n\t\tawait applyAndroidLayers(branding, config.nativeProjectDirectory);\n\tif (platforms.includes('ios'))\n\t\tawait applyIosAppearances(branding, config.nativeProjectDirectory);\n\tconst manifestPath = absoluteMobileBrandingManifestPath(projectRoot);\n\tconst completedPlatforms = await completedManifestPlatforms(\n\t\tmanifest,\n\t\tmanifestPath,\n\t\tplatforms\n\t);\n\tawait writeManifest(projectRoot, {\n\t\t...manifest,\n\t\tplatforms: completedPlatforms\n\t});\n\n\treturn {\n\t\tmanifest: { ...manifest, platforms: completedPlatforms },\n\t\tmanifestPath,\n\t\tplatforms\n\t};\n};\n\nexport type AbsoluteMobileBrandingGenerationOptions = {\n\tplatforms?: ('android' | 'ios')[];\n\trunner?: AbsoluteMobileBrandingRunner;\n};\n\nconst parseManifestState = (value: unknown) => {\n\tconst fingerprint =\n\t\ttypeof value === 'object' && value !== null\n\t\t\t? Reflect.get(value, 'fingerprint')\n\t\t\t: undefined;\n\tif (\n\t\ttypeof value !== 'object' ||\n\t\tvalue === null ||\n\t\tReflect.get(value, 'format') !== ABSOLUTE_MOBILE_BRANDING_FORMAT ||\n\t\ttypeof fingerprint !== 'string'\n\t)\n\t\tthrow new TypeError('Mobile branding manifest is invalid.');\n\n\tconst platforms = Reflect.get(value, 'platforms');\n\tif (\n\t\t!Array.isArray(platforms) ||\n\t\tplatforms.some(\n\t\t\t(platform) => platform !== 'android' && platform !== 'ios'\n\t\t)\n\t)\n\t\tthrow new TypeError('Mobile branding manifest platforms are invalid.');\n\n\tconst validPlatforms = platforms.flatMap((platform) =>\n\t\tplatform === 'android' || platform === 'ios' ? [platform] : []\n\t);\n\n\treturn { fingerprint, platforms: new Set(validPlatforms) };\n};\n\nconst brandingInspection = (value: AbsoluteMobileBrandingInspection) => value;\n\nexport const inspectAbsoluteMobileBranding = async (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tprojectRoot: string\n) => {\n\tconst manifestPath = absoluteMobileBrandingManifestPath(projectRoot);\n\tif (!config.branding)\n\t\treturn brandingInspection({\n\t\t\tconfigured: false,\n\t\t\tdetail: 'Mobile branding is not configured; native template artwork may be shipped.',\n\t\t\tmanifestPath,\n\t\t\tready: false,\n\t\t\tstatus: 'absent'\n\t\t});\n\tlet expected: AbsoluteMobileBrandingManifest;\n\ttry {\n\t\texpected = await createAbsoluteMobileBrandingManifest(\n\t\t\tconfig,\n\t\t\tprojectRoot\n\t\t);\n\t} catch (error) {\n\t\treturn brandingInspection({\n\t\t\tconfigured: true,\n\t\t\tdetail:\n\t\t\t\terror instanceof Error\n\t\t\t\t\t? error.message\n\t\t\t\t\t: 'Mobile branding sources are invalid.',\n\t\t\tmanifestPath,\n\t\t\tready: false,\n\t\t\tstatus: 'invalid'\n\t\t});\n\t}\n\ttry {\n\t\tconst current = parseManifestState(\n\t\t\tJSON.parse(await readFile(manifestPath, 'utf8'))\n\t\t);\n\t\tif (current.fingerprint !== expected.fingerprint)\n\t\t\treturn brandingInspection({\n\t\t\t\tconfigured: true,\n\t\t\t\tdetail: 'Mobile branding sources changed after the last native projection.',\n\t\t\t\tmanifestPath,\n\t\t\t\tready: false,\n\t\t\t\tstatus: 'stale'\n\t\t\t});\n\t\tconst missingPlatforms = config.platforms.filter(\n\t\t\t(platform) => !current.platforms.has(platform)\n\t\t);\n\t\tif (missingPlatforms.length > 0)\n\t\t\treturn brandingInspection({\n\t\t\t\tconfigured: true,\n\t\t\t\tdetail: `Mobile branding has not been projected for ${missingPlatforms.join(', ')}.`,\n\t\t\t\tmanifestPath,\n\t\t\t\tready: false,\n\t\t\t\tstatus: 'stale'\n\t\t\t});\n\t\tconst nativeSentinels =\n\t\t\tconfig.engine === 'expo'\n\t\t\t\t? [\n\t\t\t\t\t\tjoin(\n\t\t\t\t\t\t\tconfig.nativeProjectDirectory,\n\t\t\t\t\t\t\t'assets',\n\t\t\t\t\t\t\t'absolute-branding',\n\t\t\t\t\t\t\t'icon.png'\n\t\t\t\t\t\t)\n\t\t\t\t\t]\n\t\t\t\t: nativeBrandingSentinels(config, config.platforms);\n\t\tif (\n\t\t\t(\n\t\t\t\tawait Promise.all(nativeSentinels.map((path) => exists(path)))\n\t\t\t).some((present) => !present)\n\t\t)\n\t\t\treturn brandingInspection({\n\t\t\t\tconfigured: true,\n\t\t\t\tdetail: 'The generated native branding projection is missing.',\n\t\t\t\tmanifestPath,\n\t\t\t\tready: false,\n\t\t\t\tstatus: 'stale'\n\t\t\t});\n\n\t\treturn brandingInspection({\n\t\t\tconfigured: true,\n\t\t\tdetail: `Mobile branding ${expected.fingerprint.slice(0, 12)} is current for ${config.engine}.`,\n\t\t\tmanifestPath,\n\t\t\tready: true,\n\t\t\tstatus: 'ready'\n\t\t});\n\t} catch {\n\t\treturn brandingInspection({\n\t\t\tconfigured: true,\n\t\t\tdetail: 'Mobile branding has not been projected into the native app.',\n\t\t\tmanifestPath,\n\t\t\tready: false,\n\t\t\tstatus: 'stale'\n\t\t});\n\t}\n};\n\nconst escapeHtml = (value: string) =>\n\tvalue\n\t\t.replaceAll('&', '&')\n\t\t.replaceAll('<', '<')\n\t\t.replaceAll('>', '>')\n\t\t.replaceAll('\"', '"')\n\t\t.replaceAll(\"'\", ''');\n\nexport const renderAbsoluteMobileBrandingPreview = async (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tprojectRoot: string\n) => {\n\tconst manifest = await createAbsoluteMobileBrandingManifest(\n\t\tconfig,\n\t\tprojectRoot\n\t);\n\tconst { branding } = config;\n\tif (!branding) throw new TypeError('mobile.branding is not configured.');\n\tconst destination = join(\n\t\tprojectRoot,\n\t\t'.absolutejs',\n\t\t'mobile',\n\t\t'branding',\n\t\t'preview.html'\n\t);\n\tconst icon = `data:image/png;base64,${(await readFile(branding.icon)).toString('base64')}`;\n\tconst appName = escapeHtml(config.appName);\n\tconst html = `<!doctype html>\\n<meta charset=\"utf-8\">\\n<meta name=\"viewport\" content=\"width=device-width\">\\n<title>${appName} mobile branding</title>\\n<style>body{font:16px system-ui;margin:2rem;background:#eee;color:#111}main{display:flex;flex-wrap:wrap;gap:2rem}.card{background:white;border-radius:20px;padding:1.5rem;box-shadow:0 8px 30px #0002}.icon{width:192px;height:192px;object-fit:cover}.circle{border-radius:50%}.squircle{border-radius:24%}.splash{width:240px;height:480px;display:grid;place-items:center;background:${branding.splash.backgroundColor}}.splash img{width:${branding.splash.logoScale * 100}%}code{display:block;margin-top:1rem}</style>\\n<h1>${appName}</h1><main><section class=\"card\"><img class=\"icon squircle\" src=\"${icon}\" alt=\"Square app icon preview\"><code>Apple / legacy Android</code></section><section class=\"card\"><img class=\"icon circle\" src=\"${icon}\" alt=\"Circular Android icon preview\"><code>Android circular mask</code></section><section class=\"card splash\"><img src=\"${icon}\" alt=\"Launch screen preview\"></section></main><p>Fingerprint: <code>${manifest.fingerprint}</code></p>\\n`;\n\tawait mkdir(dirname(destination), { recursive: true });\n\tawait writeFile(destination, html);\n\n\treturn destination;\n};\n",
|
|
34
34
|
"import { createHash } from 'node:crypto';\nimport {\n\taccess,\n\tcopyFile,\n\tmkdir,\n\tmkdtemp,\n\treadFile,\n\trealpath,\n\trename,\n\trm,\n\tstat,\n\twriteFile\n} from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport {\n\tabsoluteManagedAndroidSdkRoot,\n\tdetectAbsoluteMobileHost,\n\ttype AbsoluteMobileHost\n} from './emulatorDoctor';\nimport {\n\tbuildAbsoluteAndroidGradleArtifact,\n\tfingerprintAbsoluteAndroidNativeProject,\n\ttype AbsoluteAndroidCommandOptions,\n\ttype AbsoluteAndroidCommandResult\n} from './androidEmulatorController';\nimport type { NormalizedAbsoluteMobileConfig } from './config';\nimport { buildAbsoluteExpoAndroidWslBundle } from './expoDevController';\n\nexport const ABSOLUTE_ANDROID_RELEASE_FORMAT = 1 as const;\n\nexport type AbsoluteAndroidReleaseMetadata = {\n\tappBuild: string;\n\tappId: string;\n\tartifact: string;\n\tbytes: number;\n\tengine: 'capacitor' | 'expo';\n\tformat: typeof ABSOLUTE_ANDROID_RELEASE_FORMAT;\n\tplatform: 'android';\n\treleaseId: string;\n\truntime: string;\n\tsha256: string;\n\tsigned: boolean;\n\ttype: 'aab';\n\tversionCode?: number;\n};\n\ntype MobileClientManifest = {\n\tappBuild: string;\n\tappId: string;\n\truntime: string;\n};\n\nexport type BuildAbsoluteAndroidReleaseOptions = {\n\tallowUnsigned?: boolean;\n\tandroidRoot?: string;\n\tcapture?: (\n\t\tcommand: string[],\n\t\toptions?: AbsoluteAndroidCommandOptions\n\t) => AbsoluteAndroidCommandResult;\n\tconfig: NormalizedAbsoluteMobileConfig;\n\tenv?: Record<string, string | undefined>;\n\thost?: AbsoluteMobileHost;\n\tjarsigner?: string | null;\n\toutputDirectory?: string;\n\tprojectRoot: string;\n\trun?: (\n\t\tcommand: string[],\n\t\toptions?: AbsoluteAndroidCommandOptions\n\t) => Promise<number>;\n\tprepareVersionCode?: (buildIdentity: string) => Promise<number>;\n\tsigning?: {\n\t\tkeyAlias: string;\n\t\tkeyPasswordEnvironment: string;\n\t\tkeystorePath: string;\n\t\tstorePasswordEnvironment: string;\n\t};\n\tversionCode?: number;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst requireManifest = (value: unknown): MobileClientManifest => {\n\tif (\n\t\t!isRecord(value) ||\n\t\ttypeof value.appBuild !== 'string' ||\n\t\ttypeof value.appId !== 'string' ||\n\t\ttypeof value.runtime !== 'string'\n\t) {\n\t\tthrow new TypeError('Invalid embedded AbsoluteJS mobile manifest.');\n\t}\n\n\treturn {\n\t\tappBuild: value.appBuild,\n\t\tappId: value.appId,\n\t\truntime: value.runtime\n\t};\n};\n\nconst pathExists = async (path: string) => {\n\ttry {\n\t\tawait access(path);\n\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\nconst defaultCapture = (\n\tcommand: string[],\n\toptions: AbsoluteAndroidCommandOptions = {}\n) => {\n\ttry {\n\t\tconst result = Bun.spawnSync(command, {\n\t\t\tcwd: options.cwd,\n\t\t\tenv: options.env,\n\t\t\tstderr: 'pipe',\n\t\t\tstdin: 'ignore',\n\t\t\tstdout: 'pipe'\n\t\t});\n\n\t\treturn {\n\t\t\texitCode: result.exitCode,\n\t\t\tstderr: result.stderr.toString(),\n\t\t\tstdout: result.stdout.toString()\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\texitCode: 1,\n\t\t\tstderr: error instanceof Error ? error.message : String(error),\n\t\t\tstdout: ''\n\t\t};\n\t}\n};\n\nconst verifyAabSignature = (\n\tartifactPath: string,\n\tcapture: NonNullable<BuildAbsoluteAndroidReleaseOptions['capture']>,\n\tjarsigner: string | null | undefined\n) => {\n\tconst executable =\n\t\tjarsigner === undefined ? Bun.which('jarsigner') : jarsigner;\n\tif (!executable) return null;\n\tconst result = capture([\n\t\texecutable,\n\t\t'-J-Duser.language=en',\n\t\t'-J-Duser.country=US',\n\t\t'-verify',\n\t\tartifactPath\n\t]);\n\n\treturn result.exitCode === 0 && /jar verified/iu.test(result.stdout);\n};\n\nconst signAab = (\n\tartifactPath: string,\n\tcapture: NonNullable<BuildAbsoluteAndroidReleaseOptions['capture']>,\n\tjarsigner: string,\n\tsigning: NonNullable<BuildAbsoluteAndroidReleaseOptions['signing']>\n) => {\n\tconst result = capture([\n\t\tjarsigner,\n\t\t'-keystore',\n\t\tsigning.keystorePath,\n\t\t'-storepass:env',\n\t\tsigning.storePasswordEnvironment,\n\t\t'-keypass:env',\n\t\tsigning.keyPasswordEnvironment,\n\t\tartifactPath,\n\t\tsigning.keyAlias\n\t]);\n\tif (result.exitCode !== 0)\n\t\tthrow new TypeError(\n\t\t\t'jarsigner could not sign the Android App Bundle with the configured CI identity.'\n\t\t);\n};\n\nconst windowsJarsignerPath = (\n\tpath: string,\n\tcapture: NonNullable<BuildAbsoluteAndroidReleaseOptions['capture']>\n) => {\n\tconst result = capture(['wslpath', '-w', path]);\n\tconst translated = result.stdout.trim();\n\tif (result.exitCode !== 0 || !translated) {\n\t\tthrow new TypeError(\n\t\t\t`Could not translate an Android signing path for the Windows JDK: ${path}`\n\t\t);\n\t}\n\n\treturn translated;\n};\n\nconst sha256File = async (path: string) =>\n\tcreateHash('sha256')\n\t\t.update(await readFile(path))\n\t\t.digest('hex');\n\nconst fingerprintExpoAndroidProject = async (nativeDirectory: string) => {\n\tconst root = await realpath(nativeDirectory);\n\tconst files = await Array.fromAsync(\n\t\tnew Bun.Glob('**/*').scan({ cwd: root, onlyFiles: true })\n\t);\n\tconst records = await Promise.all(\n\t\tfiles\n\t\t\t.filter((path) => {\n\t\t\t\tconst parts = path.replaceAll('\\\\', '/').split('/');\n\n\t\t\t\treturn !parts.includes('.gradle') && !parts.includes('build');\n\t\t\t})\n\t\t\t.sort()\n\t\t\t.map(async (path) => {\n\t\t\t\tconst contents = await readFile(join(root, path));\n\n\t\t\t\treturn `${path.replaceAll('\\\\', '/')}\\0${createHash('sha256').update(contents).digest('hex')}\\0`;\n\t\t\t})\n\t);\n\n\treturn createHash('sha256').update(records.join('')).digest('hex');\n};\n\nconst safeOutputDirectory = (projectRoot: string, requested?: string) => {\n\tconst root = resolve(projectRoot);\n\tconst output = resolve(\n\t\troot,\n\t\trequested ?? '.absolutejs/mobile/releases/android'\n\t);\n\tconst projectRelative = relative(root, output);\n\tif (\n\t\tprojectRelative === '..' ||\n\t\tprojectRelative.startsWith(`..${sep}`) ||\n\t\tisAbsolute(projectRelative)\n\t) {\n\t\tthrow new TypeError(\n\t\t\t'mobile build --outdir must remain inside the project.'\n\t\t);\n\t}\n\n\treturn output;\n};\n\nconst installRelease = async (\n\tartifactPath: string,\n\tmetadata: Omit<AbsoluteAndroidReleaseMetadata, 'artifact'>,\n\toutputRoot: string\n) => {\n\tconst releaseRoot = join(outputRoot, metadata.releaseId);\n\tconst artifactName = 'app-release.aab';\n\tconst destination = join(releaseRoot, artifactName);\n\tif (await pathExists(releaseRoot)) {\n\t\tconst existing = requireManifestIdentity(\n\t\t\tJSON.parse(\n\t\t\t\tawait readFile(join(releaseRoot, 'release.json'), 'utf8')\n\t\t\t),\n\t\t\tmetadata\n\t\t);\n\t\tconst [installedBytes, installedSha256] = await Promise.all([\n\t\t\tstat(destination).then(({ size }) => size),\n\t\t\tsha256File(destination)\n\t\t]);\n\t\tif (\n\t\t\tinstalledBytes !== metadata.bytes ||\n\t\t\tinstalledSha256 !== metadata.sha256\n\t\t) {\n\t\t\tthrow new TypeError(\n\t\t\t\t`Immutable Android release ${metadata.releaseId} artifact is missing or modified.`\n\t\t\t);\n\t\t}\n\n\t\treturn { artifactPath: destination, metadata: existing, releaseRoot };\n\t}\n\tawait mkdir(dirname(releaseRoot), { recursive: true });\n\tconst staging = await mkdtemp(\n\t\tjoin(dirname(releaseRoot), '.android-stage-')\n\t);\n\ttry {\n\t\tawait copyFile(artifactPath, join(staging, artifactName));\n\t\tconst complete = {\n\t\t\t...metadata,\n\t\t\tartifact: artifactName\n\t\t} satisfies AbsoluteAndroidReleaseMetadata;\n\t\tawait writeFile(\n\t\t\tjoin(staging, 'release.json'),\n\t\t\t`${JSON.stringify(complete, null, '\\t')}\\n`,\n\t\t\t{ flag: 'wx' }\n\t\t);\n\t\tawait rename(staging, releaseRoot);\n\n\t\treturn { artifactPath: destination, metadata: complete, releaseRoot };\n\t} finally {\n\t\tawait rm(staging, { force: true, recursive: true }).catch(\n\t\t\t() => undefined\n\t\t);\n\t}\n};\n\nconst requireManifestIdentity = (\n\tvalue: unknown,\n\texpected: Omit<AbsoluteAndroidReleaseMetadata, 'artifact'>\n) => {\n\tif (!isRecord(value)) {\n\t\tthrow new TypeError('Existing Android release metadata is invalid.');\n\t}\n\tconst { artifact } = value;\n\tif (\n\t\tartifact !== 'app-release.aab' ||\n\t\tObject.entries(expected).some(\n\t\t\t([key, expectedValue]) => Reflect.get(value, key) !== expectedValue\n\t\t)\n\t) {\n\t\tthrow new TypeError(\n\t\t\t`Immutable Android release ${expected.releaseId} does not match its content.`\n\t\t);\n\t}\n\n\treturn { ...expected, artifact } satisfies AbsoluteAndroidReleaseMetadata;\n};\n\nexport const buildAbsoluteAndroidRelease = async (\n\toptions: BuildAbsoluteAndroidReleaseOptions\n) => {\n\tif (options.versionCode !== undefined && options.prepareVersionCode) {\n\t\tthrow new TypeError(\n\t\t\t'Android release versionCode and prepareVersionCode cannot be combined.'\n\t\t);\n\t}\n\tif (\n\t\toptions.versionCode !== undefined &&\n\t\t(!Number.isSafeInteger(options.versionCode) ||\n\t\t\toptions.versionCode < 1 ||\n\t\t\toptions.versionCode > 2_100_000_000)\n\t) {\n\t\tthrow new TypeError(\n\t\t\t'Android versionCode must be an integer from 1 through 2100000000.'\n\t\t);\n\t}\n\tconst projectRoot = resolve(options.projectRoot);\n\tconst host = options.host ?? detectAbsoluteMobileHost();\n\tconst androidRoot =\n\t\toptions.androidRoot ??\n\t\tprocess.env.ANDROID_HOME ??\n\t\tprocess.env.ANDROID_SDK_ROOT ??\n\t\tabsoluteManagedAndroidSdkRoot(host);\n\tconst nativeDirectory = join(\n\t\toptions.config.nativeProjectDirectory,\n\t\t'android'\n\t);\n\tconst manifest = requireManifest(\n\t\tJSON.parse(\n\t\t\tawait readFile(\n\t\t\t\tjoin(\n\t\t\t\t\toptions.config.bundleDirectory,\n\t\t\t\t\t'absolute-mobile-manifest.json'\n\t\t\t\t),\n\t\t\t\t'utf8'\n\t\t\t)\n\t\t)\n\t);\n\tif (manifest.appId !== options.config.appId) {\n\t\tthrow new TypeError(\n\t\t\t'Embedded mobile manifest appId does not match mobile.appId.'\n\t\t);\n\t}\n\tlet { versionCode } = options;\n\tif (options.prepareVersionCode) {\n\t\tconst nativeFingerprint =\n\t\t\toptions.config.engine === 'expo'\n\t\t\t\t? await fingerprintExpoAndroidProject(nativeDirectory)\n\t\t\t\t: await fingerprintAbsoluteAndroidNativeProject({\n\t\t\t\t\t\tnativeDirectory\n\t\t\t\t\t});\n\t\tconst buildIdentity = createHash('sha256')\n\t\t\t.update(`${manifest.appBuild}\\0${nativeFingerprint}`)\n\t\t\t.digest('hex');\n\t\tversionCode = await options.prepareVersionCode(buildIdentity);\n\t}\n\tif (\n\t\tversionCode !== undefined &&\n\t\t(!Number.isSafeInteger(versionCode) ||\n\t\t\tversionCode < 1 ||\n\t\t\tversionCode > 2_100_000_000)\n\t) {\n\t\tthrow new TypeError(\n\t\t\t'Android versionCode must be an integer from 1 through 2100000000.'\n\t\t);\n\t}\n\tconst gradleArguments =\n\t\tversionCode === undefined\n\t\t\t? []\n\t\t\t: [`-Pandroid.injected.version.code=${versionCode}`];\n\tconst artifactPath =\n\t\toptions.config.engine === 'expo' && host === 'wsl'\n\t\t\t? await buildAbsoluteExpoAndroidWslBundle({\n\t\t\t\t\tandroidRoot,\n\t\t\t\t\tcapture: options.capture,\n\t\t\t\t\tenv: options.env,\n\t\t\t\t\tgradleArguments,\n\t\t\t\t\tproject: options.config.nativeProjectDirectory,\n\t\t\t\t\trun: options.run\n\t\t\t\t})\n\t\t\t: (\n\t\t\t\t\tawait buildAbsoluteAndroidGradleArtifact({\n\t\t\t\t\t\tcapture: options.capture,\n\t\t\t\t\t\tenv: options.env,\n\t\t\t\t\t\tgradleArguments,\n\t\t\t\t\t\tproject: {\n\t\t\t\t\t\t\tandroidRoot,\n\t\t\t\t\t\t\tconfig: options.config,\n\t\t\t\t\t\t\thost,\n\t\t\t\t\t\t\tnativeDirectory,\n\t\t\t\t\t\t\tprojectRoot\n\t\t\t\t\t\t},\n\t\t\t\t\t\trun: options.run,\n\t\t\t\t\t\ttask: 'bundleRelease'\n\t\t\t\t\t})\n\t\t\t\t).artifactPath;\n\tif (!(await pathExists(artifactPath))) {\n\t\tthrow new TypeError(\n\t\t\t`Android Gradle did not produce the expected App Bundle: ${artifactPath}`\n\t\t);\n\t}\n\tconst capture = options.capture ?? defaultCapture;\n\tconst jarsigner =\n\t\toptions.jarsigner === undefined\n\t\t\t? (Bun.which('jarsigner') ??\n\t\t\t\t(host === 'wsl' || host === 'windows'\n\t\t\t\t\t? Bun.which('jarsigner.exe')\n\t\t\t\t\t: null))\n\t\t\t: options.jarsigner;\n\tconst usesWindowsJarsigner =\n\t\thost === 'wsl' && jarsigner?.toLowerCase().endsWith('.exe');\n\tconst signerArtifactPath = usesWindowsJarsigner\n\t\t? windowsJarsignerPath(artifactPath, capture)\n\t\t: artifactPath;\n\tconst signing =\n\t\tusesWindowsJarsigner && options.signing\n\t\t\t? {\n\t\t\t\t\t...options.signing,\n\t\t\t\t\tkeystorePath: windowsJarsignerPath(\n\t\t\t\t\t\toptions.signing.keystorePath,\n\t\t\t\t\t\tcapture\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t: options.signing;\n\tlet signed = verifyAabSignature(signerArtifactPath, capture, jarsigner);\n\tif (signed === false && signing) {\n\t\tif (!jarsigner)\n\t\t\tthrow new TypeError(\n\t\t\t\t'Could not sign the Android App Bundle because jarsigner is unavailable.'\n\t\t\t);\n\t\tsignAab(signerArtifactPath, capture, jarsigner, signing);\n\t\tsigned = verifyAabSignature(signerArtifactPath, capture, jarsigner);\n\t\tif (!signed)\n\t\t\tthrow new TypeError(\n\t\t\t\t'Android App Bundle signature verification failed after CI signing.'\n\t\t\t);\n\t}\n\tif (signed === null && !options.allowUnsigned) {\n\t\tthrow new TypeError(\n\t\t\t'Could not verify the Android App Bundle signature because jarsigner is unavailable. Install a JDK, or use --unsigned only for a non-publishable build.'\n\t\t);\n\t}\n\tif (!signed && !options.allowUnsigned) {\n\t\tthrow new TypeError(\n\t\t\t'Android Gradle produced an unsigned App Bundle. Configure the release signingConfig in the source-owned Android project (prefer external Gradle properties), or pass --unsigned only for a non-publishable build.'\n\t\t);\n\t}\n\tconst [bytes, sha256] = await Promise.all([\n\t\tstat(artifactPath).then(({ size }) => size),\n\t\tsha256File(artifactPath)\n\t]);\n\tconst releaseId = `amobile_android_${sha256}`;\n\tconst metadata = {\n\t\tappBuild: manifest.appBuild,\n\t\tappId: manifest.appId,\n\t\tbytes,\n\t\tengine: options.config.engine,\n\t\tformat: ABSOLUTE_ANDROID_RELEASE_FORMAT,\n\t\tplatform: 'android',\n\t\treleaseId,\n\t\truntime: manifest.runtime,\n\t\tsha256,\n\t\tsigned: signed === true,\n\t\ttype: 'aab',\n\t\t...(versionCode === undefined ? {} : { versionCode })\n\t} satisfies Omit<AbsoluteAndroidReleaseMetadata, 'artifact'>;\n\n\treturn installRelease(\n\t\tartifactPath,\n\t\tmetadata,\n\t\tsafeOutputDirectory(projectRoot, options.outputDirectory)\n\t);\n};\n",
|
|
35
35
|
"import { access } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { isWSLEnvironment } from '../cli/utils';\n\nexport type AbsoluteMobileHost = 'macos' | 'windows' | 'linux' | 'wsl';\nexport type AbsoluteMobileDoctorStatus = 'pass' | 'warn' | 'fail' | 'skip';\nexport const ABSOLUTE_ANDROID_AVD_NAME = 'AbsoluteJS_API_36';\n\nexport type AbsoluteMobileDoctorCheck = {\n\tid: string;\n\tlabel: string;\n\tpath?: string;\n\tplatform: 'host' | 'android' | 'ios';\n\tremediation?: string;\n\tstatus: AbsoluteMobileDoctorStatus;\n};\n\nexport type InspectAbsoluteMobileToolchainOptions = {\n\tandroidRoot?: string | null;\n\tcapture?: (command: string[]) => { exitCode: number; stdout: string };\n\tenv?: Record<string, string | undefined>;\n\texists?: (path: string) => Promise<boolean>;\n\thost?: AbsoluteMobileHost;\n\twhich?: (command: string) => string | null;\n};\n\nconst captureCommand = (command: string[]) => {\n\ttry {\n\t\tconst result = Bun.spawnSync(command, {\n\t\t\tstderr: 'ignore',\n\t\t\tstdout: 'pipe'\n\t\t});\n\n\t\treturn {\n\t\t\texitCode: result.exitCode,\n\t\t\tstdout: result.stdout.toString()\n\t\t};\n\t} catch {\n\t\treturn { exitCode: 1, stdout: '' };\n\t}\n};\n\nconst hasAvailableIosRuntime = (output: string) => {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(output);\n\t\tif (typeof parsed !== 'object' || parsed === null) return false;\n\t\tconst runtimes = Reflect.get(parsed, 'runtimes');\n\t\tif (!Array.isArray(runtimes)) return false;\n\n\t\treturn runtimes.some(\n\t\t\t(runtime) =>\n\t\t\t\ttypeof runtime === 'object' &&\n\t\t\t\truntime !== null &&\n\t\t\t\tReflect.get(runtime, 'isAvailable') === true &&\n\t\t\t\ttypeof Reflect.get(runtime, 'identifier') === 'string' &&\n\t\t\t\tString(Reflect.get(runtime, 'identifier')).includes('iOS')\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n};\n\nconst windowsPathToWsl = (path: string) => {\n\tconst match = /^([a-z]):[\\\\/](.*)$/i.exec(path.trim());\n\tif (!match) return path.trim().replaceAll('\\\\', '/');\n\n\treturn `/mnt/${match[1]?.toLowerCase()}/${match[2]?.replaceAll('\\\\', '/')}`;\n};\n\nconst windowsLocalAppDataFromWsl = () => {\n\ttry {\n\t\tconst result = Bun.spawnSync(\n\t\t\t['cmd.exe', '/d', '/c', 'echo', '%LOCALAPPDATA%'],\n\t\t\t{ stderr: 'ignore', stdout: 'pipe' }\n\t\t);\n\t\tif (result.exitCode !== 0) return undefined;\n\t\tconst output = result.stdout.toString().trim();\n\t\tif (!output || output === '%LOCALAPPDATA%') return undefined;\n\n\t\treturn windowsPathToWsl(output);\n\t} catch {\n\t\treturn undefined;\n\t}\n};\n\nexport const absoluteManagedAndroidSdkRoot = (\n\thost: AbsoluteMobileHost,\n\tenv: Record<string, string | undefined> = process.env\n) => {\n\tif (host === 'windows') {\n\t\treturn join(\n\t\t\tenv.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'),\n\t\t\t'AbsoluteJS',\n\t\t\t'Android',\n\t\t\t'Sdk'\n\t\t);\n\t}\n\tif (host === 'wsl') {\n\t\tconst localAppData = windowsLocalAppDataFromWsl();\n\t\tif (localAppData) {\n\t\t\treturn join(localAppData, 'AbsoluteJS', 'Android', 'Sdk');\n\t\t}\n\t}\n\n\treturn join(homedir(), '.absolutejs', 'android-sdk');\n};\n\nconst pathExists = async (path: string) => {\n\ttry {\n\t\tawait access(path);\n\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\nexport const detectAbsoluteMobileHost = (\n\tplatform = process.platform,\n\twsl = isWSLEnvironment()\n) => {\n\tif (platform === 'darwin') return 'macos';\n\tif (platform === 'win32') return 'windows';\n\tif (platform === 'linux' && wsl) return 'wsl';\n\n\treturn 'linux';\n};\n\nconst executableNames = (host: AbsoluteMobileHost, name: string) => {\n\tif (host === 'wsl') return [`${name}.exe`, `${name}.bat`, name];\n\tif (host === 'windows') return [name, `${name}.exe`, `${name}.bat`];\n\n\treturn [name];\n};\n\nconst findExecutable = async (\n\tname: string,\n\tpaths: string[],\n\toptions: Required<\n\t\tPick<InspectAbsoluteMobileToolchainOptions, 'exists' | 'which'>\n\t>,\n\thost: AbsoluteMobileHost\n) => {\n\tconst existing = await Promise.all(\n\t\tpaths.map(async (path) =>\n\t\t\t(await options.exists(path)) ? path : undefined\n\t\t)\n\t);\n\tconst configured = existing.find((path) => path !== undefined);\n\tif (configured) return configured;\n\tfor (const candidate of executableNames(host, name)) {\n\t\tconst path = options.which(candidate);\n\t\tif (path) return path;\n\t}\n\n\treturn undefined;\n};\n\nconst toolCheck = (\n\tid: string,\n\tlabel: string,\n\tplatform: 'android' | 'ios',\n\tpath: string | undefined,\n\tremediation: string\n) =>\n\tpath\n\t\t? ({\n\t\t\t\tid,\n\t\t\t\tlabel,\n\t\t\t\tpath,\n\t\t\t\tplatform,\n\t\t\t\tstatus: 'pass'\n\t\t\t} satisfies AbsoluteMobileDoctorCheck)\n\t\t: ({\n\t\t\t\tid,\n\t\t\t\tlabel,\n\t\t\t\tplatform,\n\t\t\t\tremediation,\n\t\t\t\tstatus: 'fail'\n\t\t\t} satisfies AbsoluteMobileDoctorCheck);\n\nexport const inspectAbsoluteMobileToolchain = async (\n\tinput: InspectAbsoluteMobileToolchainOptions = {}\n) => {\n\tconst env = input.env ?? process.env;\n\tconst host = input.host ?? detectAbsoluteMobileHost();\n\tconst exists = input.exists ?? pathExists;\n\tconst which = input.which ?? ((command: string) => Bun.which(command));\n\tconst capture = input.capture ?? captureCommand;\n\tconst androidRoot =\n\t\tinput.androidRoot === null\n\t\t\t? undefined\n\t\t\t: (input.androidRoot ??\n\t\t\t\tenv.ANDROID_HOME ??\n\t\t\t\tenv.ANDROID_SDK_ROOT ??\n\t\t\t\tabsoluteManagedAndroidSdkRoot(host, env));\n\tconst windowsAndroidTools = host === 'windows' || host === 'wsl';\n\tconst android = (segments: string[]) =>\n\t\tandroidRoot ? join(androidRoot, ...segments) : undefined;\n\tconst paths = (values: Array<string | undefined>) =>\n\t\tvalues.filter((value): value is string => Boolean(value));\n\tconst adb = await findExecutable(\n\t\t'adb',\n\t\tpaths([\n\t\t\tandroid(['platform-tools', windowsAndroidTools ? 'adb.exe' : 'adb'])\n\t\t]),\n\t\t{ exists, which },\n\t\thost\n\t);\n\tconst emulator = await findExecutable(\n\t\t'emulator',\n\t\tpaths([\n\t\t\tandroid([\n\t\t\t\t'emulator',\n\t\t\t\twindowsAndroidTools ? 'emulator.exe' : 'emulator'\n\t\t\t])\n\t\t]),\n\t\t{ exists, which },\n\t\thost\n\t);\n\tconst sdkmanager = await findExecutable(\n\t\t'sdkmanager',\n\t\tpaths([\n\t\t\tandroid([\n\t\t\t\t'cmdline-tools',\n\t\t\t\t'latest',\n\t\t\t\t'bin',\n\t\t\t\twindowsAndroidTools ? 'sdkmanager.bat' : 'sdkmanager'\n\t\t\t])\n\t\t]),\n\t\t{ exists, which },\n\t\thost\n\t);\n\tconst avdmanager = await findExecutable(\n\t\t'avdmanager',\n\t\tpaths([\n\t\t\tandroid([\n\t\t\t\t'cmdline-tools',\n\t\t\t\t'latest',\n\t\t\t\t'bin',\n\t\t\t\twindowsAndroidTools ? 'avdmanager.bat' : 'avdmanager'\n\t\t\t])\n\t\t]),\n\t\t{ exists, which },\n\t\thost\n\t);\n\tconst java = await findExecutable('java', [], { exists, which }, host);\n\tconst checks: AbsoluteMobileDoctorCheck[] = [\n\t\t{\n\t\t\tid: 'host',\n\t\t\tlabel: `Development host: ${host}`,\n\t\t\tplatform: 'host',\n\t\t\tstatus: 'pass'\n\t\t},\n\t\ttoolCheck(\n\t\t\t'android.adb',\n\t\t\t'Android Debug Bridge',\n\t\t\t'android',\n\t\t\tadb,\n\t\t\t'Install Android SDK Platform Tools or expose adb on PATH.'\n\t\t),\n\t\ttoolCheck(\n\t\t\t'android.emulator',\n\t\t\t'Android Emulator',\n\t\t\t'android',\n\t\t\temulator,\n\t\t\t'Install the Android Emulator from Android Studio SDK Manager.'\n\t\t),\n\t\ttoolCheck(\n\t\t\t'android.sdkmanager',\n\t\t\t'Android SDK Manager',\n\t\t\t'android',\n\t\t\tsdkmanager,\n\t\t\t'Install Android SDK Command-line Tools (latest).'\n\t\t),\n\t\ttoolCheck(\n\t\t\t'android.avdmanager',\n\t\t\t'Android Virtual Device Manager',\n\t\t\t'android',\n\t\t\tavdmanager,\n\t\t\t'Install Android SDK Command-line Tools (latest).'\n\t\t),\n\t\ttoolCheck(\n\t\t\t'android.java',\n\t\t\t'Java runtime',\n\t\t\t'android',\n\t\t\tjava,\n\t\t\t'Install the JDK required by the configured Android Gradle plugin.'\n\t\t)\n\t];\n\tif (emulator) {\n\t\tconst avds = capture([emulator, '-list-avds']);\n\t\tconst hasManagedAvd =\n\t\t\tavds.exitCode === 0 &&\n\t\t\tavds.stdout.split(/\\r?\\n/).includes(ABSOLUTE_ANDROID_AVD_NAME);\n\t\tchecks.push({\n\t\t\tid: 'android.avd',\n\t\t\tlabel: `AbsoluteJS Android emulator (${ABSOLUTE_ANDROID_AVD_NAME})`,\n\t\t\tplatform: 'android',\n\t\t\tremediation: hasManagedAvd\n\t\t\t\t? undefined\n\t\t\t\t: 'Run absolute mobile doctor android --fix to provision the managed emulator.',\n\t\t\tstatus: hasManagedAvd ? 'pass' : 'fail'\n\t\t});\n\t}\n\n\tif (host === 'wsl') {\n\t\tchecks.push({\n\t\t\tid: 'android.virtualization',\n\t\t\tlabel: adb?.endsWith('.exe')\n\t\t\t\t? 'Windows-host Android bridge available to WSL'\n\t\t\t\t: 'WSL requires a Windows-host emulator bridge or Linux KVM',\n\t\t\tplatform: 'android',\n\t\t\tremediation: adb?.endsWith('.exe')\n\t\t\t\t? undefined\n\t\t\t\t: 'Expose the Windows Android SDK adb.exe to WSL, or enable /dev/kvm for a Linux SDK.',\n\t\t\tstatus: adb?.endsWith('.exe') ? 'pass' : 'warn'\n\t\t});\n\t} else if (host === 'linux') {\n\t\tconst hasKvm = await exists('/dev/kvm');\n\t\tchecks.push({\n\t\t\tid: 'android.virtualization',\n\t\t\tlabel: 'Linux KVM acceleration',\n\t\t\tplatform: 'android',\n\t\t\tremediation: hasKvm\n\t\t\t\t? undefined\n\t\t\t\t: 'Enable KVM and grant the current user access to /dev/kvm.',\n\t\t\tstatus: hasKvm ? 'pass' : 'warn'\n\t\t});\n\t}\n\n\tif (host !== 'macos') {\n\t\tchecks.push({\n\t\t\tid: 'ios.simulator',\n\t\t\tlabel: 'iOS Simulator requires macOS and Xcode',\n\t\t\tplatform: 'ios',\n\t\t\tstatus: 'skip'\n\t\t});\n\n\t\treturn checks;\n\t}\n\n\tconst xcrun = await findExecutable('xcrun', [], { exists, which }, host);\n\tconst xcodebuild = await findExecutable(\n\t\t'xcodebuild',\n\t\t[],\n\t\t{ exists, which },\n\t\thost\n\t);\n\tchecks.push(\n\t\ttoolCheck(\n\t\t\t'ios.xcrun',\n\t\t\t'Xcode command runner',\n\t\t\t'ios',\n\t\t\txcrun,\n\t\t\t'Install Xcode and select it with xcode-select.'\n\t\t),\n\t\ttoolCheck(\n\t\t\t'ios.xcodebuild',\n\t\t\t'Xcode build system',\n\t\t\t'ios',\n\t\t\txcodebuild,\n\t\t\t'Install Xcode and select it with xcode-select.'\n\t\t)\n\t);\n\tif (xcrun) {\n\t\tconst runtimes = capture([\n\t\t\txcrun,\n\t\t\t'simctl',\n\t\t\t'list',\n\t\t\t'runtimes',\n\t\t\t'--json'\n\t\t]);\n\t\tconst hasRuntime =\n\t\t\truntimes.exitCode === 0 && hasAvailableIosRuntime(runtimes.stdout);\n\t\tchecks.push({\n\t\t\tid: 'ios.runtime',\n\t\t\tlabel: 'iOS Simulator runtime',\n\t\t\tplatform: 'ios',\n\t\t\tremediation: hasRuntime\n\t\t\t\t? undefined\n\t\t\t\t: 'Run absolute mobile doctor ios --fix to download an iOS Simulator runtime.',\n\t\t\tstatus: hasRuntime ? 'pass' : 'fail'\n\t\t});\n\t}\n\n\treturn checks;\n};\n",
|
|
36
|
-
"import { $ } from 'bun';\nimport { execSync } from 'node:child_process';\nimport { existsSync, readFileSync, realpathSync } from 'node:fs';\nimport { createServer } from 'node:net';\nimport { resolve } from 'node:path';\nimport type { DbScripts } from '../../types/cli';\nimport { MILLISECONDS_IN_A_SECOND } from '../constants';\nimport { formatTimestamp } from '../utils/startupBanner';\n\nexport const COMPOSE_PATH = 'db/docker-compose.db.yml';\nexport const DEFAULT_SERVER_ENTRY = 'src/backend/server.ts';\nexport const isWSLEnvironment = () => {\n\ttry {\n\t\tconst release = readFileSync('/proc/version', 'utf-8');\n\n\t\treturn /microsoft|wsl/i.test(release);\n\t} catch {\n\t\treturn false;\n\t}\n};\nconst safeKill = (pid: number) => {\n\ttry {\n\t\tprocess.kill(pid, 'SIGTERM');\n\t} catch {\n\t\t/* already exited */\n\t}\n};\n\n// Ask the OS for an unused TCP port (bind :0, read the assigned port, release it).\n// Used to pick a collision-proof port for the compile pre-render server: the old\n// `DEFAULT_PORT + 1` is frequently occupied on CI runners, and the `lsof`-based\n// stale-process cleanup is a no-op on minimal images where `lsof` isn't installed —\n// together those produced an EADDRINUSE that failed every runner compile.\nexport const findFreePort = () =>\n\tnew Promise<number>((_resolve, reject) => {\n\t\tconst server = createServer();\n\t\tserver.unref();\n\t\tserver.once('error', reject);\n\t\tserver.listen(0, '127.0.0.1', () => {\n\t\t\tconst address = server.address();\n\t\t\tif (!address || typeof address === 'string') {\n\t\t\t\tserver.close();\n\t\t\t\treject(new Error('Failed to allocate a free port'));\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tserver.close((error) => {\n\t\t\t\tif (error) {\n\t\t\t\t\treject(error);\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_resolve(address.port);\n\t\t\t});\n\t\t});\n\t});\n\nconst tryRead = <T>(read: () => T) => {\n\ttry {\n\t\treturn { ok: true as const, value: read() };\n\t} catch {\n\t\treturn { ok: false as const };\n\t}\n};\n\n/** The first reader that succeeds; later readers run only when needed. */\nconst firstReadable = <T>(fallback: T, ...readers: (() => T)[]) => {\n\tfor (const read of readers) {\n\t\tconst result = tryRead(read);\n\t\tif (result.ok) return result.value;\n\t}\n\n\treturn fallback;\n};\n\nconst processCommand = (pid: number) =>\n\tfirstReadable(\n\t\t'',\n\t\t() =>\n\t\t\treadFileSync(`/proc/${pid}/cmdline`, 'utf-8')\n\t\t\t\t.split('\\0')\n\t\t\t\t.join(' ')\n\t\t\t\t.trim(),\n\t\t() =>\n\t\t\texecSync(`ps -o command= -p ${pid}`, {\n\t\t\t\tencoding: 'utf-8'\n\t\t\t}).trim()\n\t);\n\nconst listedDirectory = (pid: number) => {\n\tconst listed = execSync(`lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`, {\n\t\tencoding: 'utf-8'\n\t});\n\tconst line = listed.split('\\n').find((entry) => entry.startsWith('n'));\n\n\treturn line ? line.slice(1) : null;\n};\n\nconst processDirectory = (pid: number) =>\n\tfirstReadable<string | null>(\n\t\tnull,\n\t\t() => realpathSync(`/proc/${pid}/cwd`),\n\t\t() => listedDirectory(pid)\n\t);\n\nconst ABSOLUTE_PROCESS =\n\t/@absolutejs[\\\\/+]absolute|serverBootstrap|[\\\\/]\\.absolutejs[\\\\/]|\\babsolute (?:dev|start|compile|preview)\\b/;\n\n/**\n * Whether a process holding the port is a leftover of this project's own\n * AbsoluteJS server, and so safe to replace. Anything else on the port -- an\n * unrelated service, or another project's server -- is someone else's and is\n * left running.\n */\nexport const isOwnStaleProcess = (\n\tcommand: string,\n\tdirectory: string | null,\n\tprojectDirectory: string\n) =>\n\tABSOLUTE_PROCESS.test(command) &&\n\t(directory === projectDirectory || command.includes(projectDirectory));\n\nexport const killStaleProcesses = (\n\tport: number,\n\tlogMessage?: (message: string) => void\n) => {\n\tlet output: string;\n\ttry {\n\t\toutput = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null`, {\n\t\t\tencoding: 'utf-8'\n\t\t}).trim();\n\t} catch {\n\t\treturn;\n\t}\n\tif (!output) {\n\t\treturn;\n\t}\n\n\tconst report = (message: string) => {\n\t\tif (logMessage) {\n\t\t\tlogMessage(message);\n\n\t\t\treturn;\n\t\t}\n\t\tconsole.log(\n\t\t\t`\\x1b[2m${formatTimestamp()}\\x1b[0m \\x1b[33m[cli]\\x1b[0m \\x1b[33m${message}\\x1b[0m`\n\t\t);\n\t};\n\tconst pids = output\n\t\t.split('\\n')\n\t\t.map(Number)\n\t\t.filter((pid) => pid !== process.pid && pid > 0);\n\tif (pids.length === 0) {\n\t\treturn;\n\t}\n\n\tconst projectDirectory = realpathSync(process.cwd());\n\tconst holders = pids.map((pid) => ({\n\t\tcommand: processCommand(pid),\n\t\tdirectory: processDirectory(pid),\n\t\tpid\n\t}));\n\tconst own = holders.filter((holder) =>\n\t\tisOwnStaleProcess(holder.command, holder.directory, projectDirectory)\n\t);\n\tconst foreign = holders.filter((holder) => !own.includes(holder));\n\town.forEach((holder) => safeKill(holder.pid));\n\tif (own.length > 0)\n\t\treport(\n\t\t\t`Killed ${own.length} stale ${own.length === 1 ? 'process' : 'processes'} on port ${port}.`\n\t\t);\n\tfor (const holder of foreign)\n\t\treport(\n\t\t\t`Port ${port} is in use by another process (pid ${holder.pid}: ${holder.command.slice(0, 120) || 'unknown'}). It was left running; stop it or choose another port.`\n\t\t);\n};\nexport const openUrlInBrowser = (\n\turl: string,\n\tonError?: (message: string) => void\n) => {\n\tif (process.env.ABSOLUTE_NO_OPEN) return false;\n\n\tconst { platform } = process;\n\tconst isWSL = platform === 'linux' && isWSLEnvironment();\n\tlet command: string;\n\tif (isWSL) {\n\t\tcommand = 'cmd.exe';\n\t} else if (platform === 'darwin') {\n\t\tcommand = 'open';\n\t} else if (platform === 'win32') {\n\t\tcommand = 'start';\n\t} else {\n\t\tcommand = 'xdg-open';\n\t}\n\tconst commandArgs = isWSL ? ['/c', 'start', url] : [url];\n\ttry {\n\t\tBun.spawn([command, ...commandArgs], {\n\t\t\tstderr: 'ignore',\n\t\t\tstdout: 'ignore'\n\t\t});\n\n\t\treturn true;\n\t} catch {\n\t\tonError?.(`Could not open browser automatically. Visit ${url}`);\n\n\t\treturn false;\n\t}\n};\n\nexport const printHelp = (subject = 'server', mobile = false) => {\n\tconst title = subject === 'workspace' ? 'workspace' : subject;\n\tconsole.log('');\n\tconsole.log('\\x1b[1mShortcuts:\\x1b[0m');\n\tconsole.log(` \\x1b[36mr\\x1b[0m / restart — Restart ${title}`);\n\tconsole.log(` \\x1b[36mp\\x1b[0m / pause — Pause/resume ${title}`);\n\tconsole.log(' \\x1b[36mo\\x1b[0m / open — Open in browser');\n\tconsole.log(' \\x1b[36mc\\x1b[0m / clear — Clear terminal');\n\tconsole.log(' \\x1b[36mm\\x1b[0m / heap — Write a heap snapshot');\n\tif (mobile) {\n\t\tconsole.log(\n\t\t\t' \\x1b[36md\\x1b[0m / device — Show mobile target status'\n\t\t);\n\t\tconsole.log(' relaunch — Relaunch the mobile app');\n\t}\n\tconsole.log(' \\x1b[36mq\\x1b[0m / quit — Graceful shutdown');\n\tconsole.log(' \\x1b[36mh\\x1b[0m / help — Show this help');\n\tconsole.log(' \\x1b[36m$\\x1b[0m — Run a shell command');\n\tconsole.log(\n\t\t' \\x1b[36m↑\\x1b[0m / \\x1b[36m↓\\x1b[0m — Command history'\n\t);\n\tconsole.log('');\n};\nexport const printHint = () => {\n\tconsole.log('\\x1b[90mpress h + enter to show shortcuts\\x1b[0m');\n};\nexport const readDbScripts = async () => {\n\tconst pkgPath = resolve('package.json');\n\tif (!existsSync(pkgPath)) return null;\n\n\tconst pkg = await Bun.file(pkgPath).json();\n\tconst upCommand: string | undefined = pkg.scripts?.['db:up'];\n\tconst downCommand: string | undefined = pkg.scripts?.['db:down'];\n\n\tif (!upCommand || !downCommand) return null;\n\n\treturn { downCommand, upCommand };\n};\nexport const startDatabase = async (scripts: DbScripts) => {\n\tawait timed('Starting database container...', async () => {\n\t\tconst { exitCode } = await $`${{ raw: scripts.upCommand }}`\n\t\t\t.quiet()\n\t\t\t.nothrow();\n\t\tif (exitCode !== 0) process.exit(exitCode);\n\t});\n};\nexport const stopDatabase = async (scripts: DbScripts) => {\n\tconsole.log('\\nStopping database container...');\n\tawait $`${{ raw: scripts.downCommand }}`.quiet().nothrow();\n};\nexport const timed = async (label: string, task: () => Promise<void>) => {\n\tprocess.stdout.write(label);\n\tconst start = performance.now();\n\tawait task();\n\tconst duration = (\n\t\t(performance.now() - start) /\n\t\tMILLISECONDS_IN_A_SECOND\n\t).toFixed(2);\n\tprocess.stdout.write(` \\x1b[90m${duration}s\\x1b[0m\\n`);\n};\n",
|
|
36
|
+
"import { $ } from 'bun';\nimport { execSync } from 'node:child_process';\nimport { existsSync, readFileSync, realpathSync } from 'node:fs';\nimport { createServer } from 'node:net';\nimport { resolve } from 'node:path';\nimport type { DbScripts } from '../../types/cli';\nimport { MILLISECONDS_IN_A_SECOND } from '../constants';\nimport { formatTimestamp } from '../utils/startupBanner';\n\nexport const COMPOSE_PATH = 'db/docker-compose.db.yml';\nexport const DEFAULT_SERVER_ENTRY = 'src/backend/server.ts';\nexport const isWSLEnvironment = () => {\n\ttry {\n\t\tconst release = readFileSync('/proc/version', 'utf-8');\n\n\t\treturn /microsoft|wsl/i.test(release);\n\t} catch {\n\t\treturn false;\n\t}\n};\nconst safeKill = (pid: number) => {\n\ttry {\n\t\tprocess.kill(pid, 'SIGTERM');\n\t} catch {\n\t\t/* already exited */\n\t}\n};\n\n// Ask the OS for an unused TCP port (bind :0, read the assigned port, release it).\n// Used to pick a collision-proof port for the compile pre-render server: the old\n// `DEFAULT_PORT + 1` is frequently occupied on CI runners, and the `lsof`-based\n// stale-process cleanup is a no-op on minimal images where `lsof` isn't installed —\n// together those produced an EADDRINUSE that failed every runner compile.\nexport const findFreePort = () =>\n\tnew Promise<number>((_resolve, reject) => {\n\t\tconst server = createServer();\n\t\tserver.unref();\n\t\tserver.once('error', reject);\n\t\tserver.listen(0, '127.0.0.1', () => {\n\t\t\tconst address = server.address();\n\t\t\tif (!address || typeof address === 'string') {\n\t\t\t\tserver.close();\n\t\t\t\treject(new Error('Failed to allocate a free port'));\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tserver.close((error) => {\n\t\t\t\tif (error) {\n\t\t\t\t\treject(error);\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_resolve(address.port);\n\t\t\t});\n\t\t});\n\t});\n\nconst tryRead = <T>(read: () => T) => {\n\ttry {\n\t\treturn { ok: true as const, value: read() };\n\t} catch {\n\t\treturn { ok: false as const };\n\t}\n};\n\n/** The first reader that succeeds; later readers run only when needed. */\nconst firstReadable = <T>(fallback: T, ...readers: (() => T)[]) => {\n\tfor (const read of readers) {\n\t\tconst result = tryRead(read);\n\t\tif (result.ok) return result.value;\n\t}\n\n\treturn fallback;\n};\n\nconst processCommand = (pid: number) =>\n\tfirstReadable(\n\t\t'',\n\t\t() =>\n\t\t\treadFileSync(`/proc/${pid}/cmdline`, 'utf-8')\n\t\t\t\t.split('\\0')\n\t\t\t\t.join(' ')\n\t\t\t\t.trim(),\n\t\t() =>\n\t\t\texecSync(`ps -o command= -p ${pid}`, {\n\t\t\t\tencoding: 'utf-8'\n\t\t\t}).trim()\n\t);\n\nconst listedDirectory = (pid: number) => {\n\tconst listed = execSync(`lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`, {\n\t\tencoding: 'utf-8'\n\t});\n\tconst line = listed.split('\\n').find((entry) => entry.startsWith('n'));\n\n\treturn line ? line.slice(1) : null;\n};\n\nconst processDirectory = (pid: number) =>\n\tfirstReadable<string | null>(\n\t\tnull,\n\t\t() => realpathSync(`/proc/${pid}/cwd`),\n\t\t() => listedDirectory(pid)\n\t);\n\nconst ABSOLUTE_PROCESS =\n\t/@absolutejs[\\\\/+]absolute|serverBootstrap|[\\\\/]\\.absolutejs[\\\\/]|\\babsolute (?:dev|start|compile|preview)\\b/;\n\n/**\n * Whether a process holding the port is a leftover of this project's own\n * AbsoluteJS server, and so safe to replace. Anything else on the port -- an\n * unrelated service, or another project's server -- is someone else's and is\n * left running.\n */\nexport const isOwnStaleProcess = (\n\tcommand: string,\n\tdirectory: string | null,\n\tprojectDirectory: string\n) =>\n\tABSOLUTE_PROCESS.test(command) &&\n\t(directory === projectDirectory || command.includes(projectDirectory));\n\nexport const killStaleProcesses = (\n\tport: number,\n\tlogMessage?: (message: string) => void\n) => {\n\tlet output: string;\n\ttry {\n\t\toutput = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null`, {\n\t\t\tencoding: 'utf-8'\n\t\t}).trim();\n\t} catch {\n\t\treturn [];\n\t}\n\tif (!output) {\n\t\treturn [];\n\t}\n\n\tconst report = (message: string) => {\n\t\tif (logMessage) {\n\t\t\tlogMessage(message);\n\n\t\t\treturn;\n\t\t}\n\t\tconsole.log(\n\t\t\t`\\x1b[2m${formatTimestamp()}\\x1b[0m \\x1b[33m[cli]\\x1b[0m \\x1b[33m${message}\\x1b[0m`\n\t\t);\n\t};\n\tconst pids = output\n\t\t.split('\\n')\n\t\t.map(Number)\n\t\t.filter((pid) => pid !== process.pid && pid > 0);\n\tif (pids.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst projectDirectory = realpathSync(process.cwd());\n\tconst holders = pids.map((pid) => ({\n\t\tcommand: processCommand(pid),\n\t\tdirectory: processDirectory(pid),\n\t\tpid\n\t}));\n\tconst own = holders.filter((holder) =>\n\t\tisOwnStaleProcess(holder.command, holder.directory, projectDirectory)\n\t);\n\tconst foreign = holders.filter((holder) => !own.includes(holder));\n\town.forEach((holder) => safeKill(holder.pid));\n\tif (own.length > 0)\n\t\treport(\n\t\t\t`Killed ${own.length} stale ${own.length === 1 ? 'process' : 'processes'} on port ${port}.`\n\t\t);\n\tfor (const holder of foreign)\n\t\treport(\n\t\t\t`Port ${port} is in use by another process (pid ${holder.pid}: ${holder.command.slice(0, 120) || 'unknown'}). It was left running; stop it or choose another port.`\n\t\t);\n\n\t/* What still holds the port: a caller that needs it (a workspace service)\n\t * must not go on to probe a server that isn't its own. */\n\treturn foreign;\n};\nexport const openUrlInBrowser = (\n\turl: string,\n\tonError?: (message: string) => void\n) => {\n\tif (process.env.ABSOLUTE_NO_OPEN) return false;\n\n\tconst { platform } = process;\n\tconst isWSL = platform === 'linux' && isWSLEnvironment();\n\tlet command: string;\n\tif (isWSL) {\n\t\tcommand = 'cmd.exe';\n\t} else if (platform === 'darwin') {\n\t\tcommand = 'open';\n\t} else if (platform === 'win32') {\n\t\tcommand = 'start';\n\t} else {\n\t\tcommand = 'xdg-open';\n\t}\n\tconst commandArgs = isWSL ? ['/c', 'start', url] : [url];\n\ttry {\n\t\tBun.spawn([command, ...commandArgs], {\n\t\t\tstderr: 'ignore',\n\t\t\tstdout: 'ignore'\n\t\t});\n\n\t\treturn true;\n\t} catch {\n\t\tonError?.(`Could not open browser automatically. Visit ${url}`);\n\n\t\treturn false;\n\t}\n};\n\nexport const printHelp = (subject = 'server', mobile = false) => {\n\tconst title = subject === 'workspace' ? 'workspace' : subject;\n\tconsole.log('');\n\tconsole.log('\\x1b[1mShortcuts:\\x1b[0m');\n\tconsole.log(` \\x1b[36mr\\x1b[0m / restart — Restart ${title}`);\n\tconsole.log(` \\x1b[36mp\\x1b[0m / pause — Pause/resume ${title}`);\n\tconsole.log(' \\x1b[36mo\\x1b[0m / open — Open in browser');\n\tconsole.log(' \\x1b[36mc\\x1b[0m / clear — Clear terminal');\n\tconsole.log(' \\x1b[36mm\\x1b[0m / heap — Write a heap snapshot');\n\tif (mobile) {\n\t\tconsole.log(\n\t\t\t' \\x1b[36md\\x1b[0m / device — Show mobile target status'\n\t\t);\n\t\tconsole.log(' relaunch — Relaunch the mobile app');\n\t}\n\tconsole.log(' \\x1b[36mq\\x1b[0m / quit — Graceful shutdown');\n\tconsole.log(' \\x1b[36mh\\x1b[0m / help — Show this help');\n\tconsole.log(' \\x1b[36m$\\x1b[0m — Run a shell command');\n\tconsole.log(\n\t\t' \\x1b[36m↑\\x1b[0m / \\x1b[36m↓\\x1b[0m — Command history'\n\t);\n\tconsole.log('');\n};\nexport const printHint = () => {\n\tconsole.log('\\x1b[90mpress h + enter to show shortcuts\\x1b[0m');\n};\nexport const readDbScripts = async () => {\n\tconst pkgPath = resolve('package.json');\n\tif (!existsSync(pkgPath)) return null;\n\n\tconst pkg = await Bun.file(pkgPath).json();\n\tconst upCommand: string | undefined = pkg.scripts?.['db:up'];\n\tconst downCommand: string | undefined = pkg.scripts?.['db:down'];\n\n\tif (!upCommand || !downCommand) return null;\n\n\treturn { downCommand, upCommand };\n};\nexport const startDatabase = async (scripts: DbScripts) => {\n\tawait timed('Starting database container...', async () => {\n\t\tconst { exitCode } = await $`${{ raw: scripts.upCommand }}`\n\t\t\t.quiet()\n\t\t\t.nothrow();\n\t\tif (exitCode !== 0) process.exit(exitCode);\n\t});\n};\nexport const stopDatabase = async (scripts: DbScripts) => {\n\tconsole.log('\\nStopping database container...');\n\tawait $`${{ raw: scripts.downCommand }}`.quiet().nothrow();\n};\nexport const timed = async (label: string, task: () => Promise<void>) => {\n\tprocess.stdout.write(label);\n\tconst start = performance.now();\n\tawait task();\n\tconst duration = (\n\t\t(performance.now() - start) /\n\t\tMILLISECONDS_IN_A_SECOND\n\t).toFixed(2);\n\tprocess.stdout.write(` \\x1b[90m${duration}s\\x1b[0m\\n`);\n};\n",
|
|
37
37
|
"import {\n\taccess,\n\tcopyFile,\n\tlstat,\n\tmkdir,\n\treadFile,\n\treaddir,\n\treadlink,\n\trealpath,\n\trename,\n\trm,\n\twriteFile\n} from 'node:fs/promises';\nimport { createHash, randomUUID } from 'node:crypto';\nimport {\n\tdirname,\n\tisAbsolute,\n\tjoin,\n\trelative,\n\tresolve,\n\tsep,\n\twin32\n} from 'node:path';\nimport {\n\tABSOLUTE_ANDROID_AVD_NAME,\n\tabsoluteManagedAndroidSdkRoot,\n\tdetectAbsoluteMobileHost,\n\tinspectAbsoluteMobileToolchain,\n\ttype AbsoluteMobileHost\n} from './emulatorDoctor';\nimport type { NormalizedAbsoluteMobileConfig } from './config';\nimport { writeAbsoluteCapacitorConfig } from './capacitorProject';\nimport { getDurationString } from '../utils/getDurationString';\n\nconst ANDROID_BOOT_TIMEOUT_MS = 180_000;\nconst ANDROID_BOOT_POLL_MS = 1_000;\nconst ANDROID_CAPTURE_TIMEOUT_MS = 10_000;\nconst DEV_JOURNAL_FORMAT = 1;\nconst NATIVE_CACHE_FORMAT = 1;\nconst HASH_RADIX = 16;\nconst EXECUTABLE_MODE_MASK = 0o111;\nconst NATIVE_PUBLIC_PATH_SEGMENTS = 5;\nconst CAPACITOR_PROJECT_DIRECTORY_PATTERN =\n\t/project\\(['\"](:[^'\"]+)['\"]\\)\\.projectDir\\s*=\\s*new File\\(['\"]([^'\"]+)['\"]\\)/gu;\n\nexport type AbsoluteAndroidCommandOptions = {\n\tcwd?: string;\n\tenv?: Record<string, string | undefined>;\n\tsignal?: AbortSignal;\n};\n\nexport type AbsoluteAndroidCommandResult = {\n\texitCode: number;\n\tstderr: string;\n\tstdout: string;\n};\n\nexport type AbsoluteAndroidDevState =\n\t| 'booting'\n\t| 'building'\n\t| 'closed'\n\t| 'closing'\n\t| 'configuring'\n\t| 'connecting'\n\t| 'failed'\n\t| 'forwarding'\n\t| 'installing'\n\t| 'checking-native'\n\t| 'launching'\n\t| 'ready'\n\t| 'streaming-logs'\n\t| 'syncing';\n\nexport type AbsoluteAndroidNativeLogEntry = {\n\tlevel: 'debug' | 'error' | 'fatal' | 'info' | 'verbose' | 'warn';\n\tmessage: string;\n\ttag: string;\n};\n\ntype AbsoluteAndroidLogStream = {\n\tclose: () => Promise<void>;\n};\n\ntype AndroidDevJournal = {\n\tbackupPath: string;\n\tformat: number;\n\tmanifestBackupPath?: string;\n\tnativeConfigPath: string;\n\tnativeManifestPath?: string;\n\tprojectedFiles?: Array<{\n\t\tbackupPath?: string;\n\t\tpath: string;\n\t}>;\n};\n\ntype AndroidNativeCache = {\n\tappId: string;\n\tfingerprint: string;\n\tformat: number;\n\tinstallations: Record<string, string>;\n};\n\ntype AndroidInstalledPackage = {\n\tidentity: string;\n\tuid?: string;\n};\n\nexport type AbsoluteAndroidDevProject = {\n\tadb: string;\n\tandroidRoot: string;\n\tcap: string;\n\tconfig: NormalizedAbsoluteMobileConfig;\n\temulator?: string;\n\thost: AbsoluteMobileHost;\n\tnativeDirectory: string;\n\tprojectRoot: string;\n};\n\nexport type AbsoluteAndroidDevSession = {\n\tclose: () => Promise<void>;\n\tnativeCacheHit: boolean;\n\trebuild: () => Promise<AbsoluteAndroidDevSession>;\n\trelaunch: () => Promise<void>;\n\tserial: string;\n\tstate: AbsoluteAndroidDevState;\n\tstartedEmulator: boolean;\n\ttimings: Record<string, number>;\n};\n\nexport type AbsoluteAndroidDevPhaseTiming = {\n\tdurationMs: number;\n\tphase: AbsoluteAndroidDevState;\n\ttotalMs: number;\n};\n\nconst ANDROID_TIMING_PHASES: Array<[string, string]> = [\n\t['syncing', 'Capacitor sync'],\n\t['configuring', 'dev config'],\n\t['fingerprinting', 'fingerprint'],\n\t['booting', 'emulator'],\n\t['connecting', 'device ready'],\n\t['forwarding', 'ADB reverse'],\n\t['checking-native', 'package check'],\n\t['building', 'Gradle'],\n\t['installing', 'install'],\n\t['launching', 'launch'],\n\t['streaming-logs', 'logs']\n];\n\nconst androidTimingSummary = (\n\ttimings: Record<string, number>,\n\tphysicalDevice = false\n) =>\n\tANDROID_TIMING_PHASES.map(([phase, label]) => {\n\t\tconst duration = timings[phase];\n\t\tif (duration === undefined || label === undefined) return null;\n\t\tlet phaseLabel = label;\n\t\tif (physicalDevice && phase === 'booting')\n\t\t\tphaseLabel = 'device selection';\n\t\tif (physicalDevice && phase === 'forwarding')\n\t\t\tphaseLabel = 'LAN transport';\n\n\t\treturn `${phaseLabel} ${getDurationString(duration)}`;\n\t})\n\t\t.filter((value): value is string => value !== null)\n\t\t.join(', ');\n\nexport type PrepareAbsoluteAndroidDevOptions = {\n\tcreateNativeProject: boolean;\n\tprojectRoot: string;\n\ttarget?: 'device' | 'emulator';\n\trun?: (\n\t\tcommand: string[],\n\t\toptions?: AbsoluteAndroidCommandOptions\n\t) => Promise<number>;\n};\n\nexport type StartAbsoluteAndroidDevOptions = {\n\tcapture?: (\n\t\tcommand: string[],\n\t\toptions?: AbsoluteAndroidCommandOptions\n\t) => AbsoluteAndroidCommandResult;\n\tlog?: (message: string) => void;\n\tnativeLog?: (entry: AbsoluteAndroidNativeLogEntry) => void;\n\tonPhaseTiming?: (timing: AbsoluteAndroidDevPhaseTiming) => void;\n\tonStateChange?: (state: AbsoluteAndroidDevState) => void;\n\thttps?: boolean;\n\t/** Launch the locally embedded production web bundle while forwarding\n\t * `port` only for its configured backend. No dev-server URL is written. */\n\tembeddedBundle?: boolean;\n\t/** A connected physical device selected explicitly by ADB serial. */\n\tdeviceSerial?: string;\n\t/** CA certificate temporarily trusted only by this development app. */\n\tcertificateAuthorityPath?: string;\n\tport: number;\n\tproject: AbsoluteAndroidDevProject;\n\t/** Hostname or LAN address loaded by the physical device. */\n\tserverHost?: string;\n\trun?: (\n\t\tcommand: string[],\n\t\toptions?: AbsoluteAndroidCommandOptions\n\t) => Promise<number>;\n\tsignal?: AbortSignal;\n\tsleep?: (milliseconds: number) => Promise<void>;\n\tspawn?: (\n\t\tcommand: string[],\n\t\toptions?: AbsoluteAndroidCommandOptions\n\t) => void;\n\tstartNativeLogs?: (\n\t\tcommand: string[],\n\t\toptions: AbsoluteAndroidCommandOptions,\n\t\tonLine: (line: string) => void\n\t) => AbsoluteAndroidLogStream;\n};\n\nconst pathExists = async (path: string) => {\n\ttry {\n\t\tawait access(path);\n\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\nconst forwardCommandOutput = (\n\tstream: ReadableStream<Uint8Array>,\n\tdestination: NodeJS.WriteStream\n) =>\n\tstream.pipeTo(\n\t\tnew WritableStream({\n\t\t\twrite: (chunk) => {\n\t\t\t\tdestination.write(chunk);\n\t\t\t}\n\t\t})\n\t);\n\nconst runCommand = async (\n\tcommand: string[],\n\toptions: AbsoluteAndroidCommandOptions = {}\n) => {\n\tconst subprocess = Bun.spawn(command, {\n\t\tcwd: options.cwd,\n\t\tenv: options.env,\n\t\tsignal: options.signal,\n\t\tstderr: 'pipe',\n\t\tstdin: 'ignore',\n\t\tstdout: 'pipe'\n\t});\n\tconst [exitCode] = await Promise.all([\n\t\tsubprocess.exited,\n\t\tforwardCommandOutput(subprocess.stdout, process.stdout),\n\t\tforwardCommandOutput(subprocess.stderr, process.stderr)\n\t]);\n\n\treturn exitCode;\n};\n\nconst captureCommand = (\n\tcommand: string[],\n\toptions: AbsoluteAndroidCommandOptions = {}\n) => {\n\ttry {\n\t\tconst result = Bun.spawnSync(command, {\n\t\t\tcwd: options.cwd,\n\t\t\tenv: options.env,\n\t\t\tstderr: 'pipe',\n\t\t\tstdout: 'pipe',\n\t\t\ttimeout: ANDROID_CAPTURE_TIMEOUT_MS\n\t\t});\n\n\t\treturn {\n\t\t\texitCode: result.exitCode,\n\t\t\tstderr: result.stderr.toString(),\n\t\t\tstdout: result.stdout.toString()\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\texitCode: 1,\n\t\t\tstderr: error instanceof Error ? error.message : String(error),\n\t\t\tstdout: ''\n\t\t};\n\t}\n};\n\nconst spawnCommand = (\n\tcommand: string[],\n\toptions: AbsoluteAndroidCommandOptions = {}\n) => {\n\tBun.spawn(command, {\n\t\tcwd: options.cwd,\n\t\tenv: options.env,\n\t\tstderr: 'ignore',\n\t\tstdin: 'ignore',\n\t\tstdout: 'ignore'\n\t});\n};\n\nconst LOGCAT_LINE_PATTERN =\n\t/^\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2}\\.\\d+\\s+\\d+\\s+\\d+\\s+([VDIWEF])\\s+([^:]+):\\s?(.*)$/u;\nconst ASCII_CONTROL_END = 31;\nconst ASCII_DELETE = 127;\nconst ASCII_ESCAPE = 27;\nconst ASCII_HORIZONTAL_TAB = 9;\nconst LOGCAT_MESSAGE_CAPTURE = 3;\nconst UNFOUND_INDEX = -1;\nconst ANSI_SEQUENCE_PATTERN = new RegExp(\n\t`${String.fromCharCode(ASCII_ESCAPE)}(?:\\\\[[0-?]*[ -/]*[@-~]|[@-_])`,\n\t'gu'\n);\nconst AUTHORIZATION_SECRET_PATTERN =\n\t/([\"']?(?:authorization|proxy-authorization)[\"']?\\s*[:=]\\s*)(?:Bearer|Basic)\\s+[^\\s,;]+/giu;\nconst BEARER_SECRET_PATTERN = /\\b(Bearer)\\s+[^\\s,;]+/giu;\nconst COOKIE_SECRET_PATTERN =\n\t/([\"']?(?:cookie|set-cookie)[\"']?\\s*[:=]\\s*).*/giu;\nconst NAMED_SECRET_PATTERN =\n\t/([\"']?(?:access_token|refresh_token|id_token|client_secret|password|code|token)[\"']?\\s*[:=]\\s*)(\"[^\"]*\"|'[^']*'|[^\\s&,;]+)/giu;\nconst QUERY_SECRET_PATTERN = /([?&](?:code|token)=)[^&\\s]+/giu;\n\nexport const redactAbsoluteAndroidLog = (value: string) =>\n\tvalue\n\t\t.replace(ANSI_SEQUENCE_PATTERN, '')\n\t\t.split('')\n\t\t.filter((character) => {\n\t\t\tconst code = character.charCodeAt(0);\n\n\t\t\treturn (\n\t\t\t\tcode === ASCII_HORIZONTAL_TAB ||\n\t\t\t\t(code > ASCII_CONTROL_END && code !== ASCII_DELETE)\n\t\t\t);\n\t\t})\n\t\t.join('')\n\t\t.replace(AUTHORIZATION_SECRET_PATTERN, '$1[REDACTED]')\n\t\t.replace(BEARER_SECRET_PATTERN, '$1 [REDACTED]')\n\t\t.replace(COOKIE_SECRET_PATTERN, '$1[REDACTED]')\n\t\t.replace(NAMED_SECRET_PATTERN, '$1[REDACTED]')\n\t\t.replace(QUERY_SECRET_PATTERN, '$1[REDACTED]');\n\nconst androidLogLevel = (level: string) => {\n\tif (level === 'V') return 'verbose';\n\tif (level === 'D') return 'debug';\n\tif (level === 'W') return 'warn';\n\tif (level === 'E') return 'error';\n\tif (level === 'F') return 'fatal';\n\n\treturn 'info';\n};\n\nconst emitCompleteLogLines = (\n\tvalue: string,\n\tonLine: (line: string) => void\n) => {\n\tlet buffered = value;\n\tlet newlineIndex = buffered.indexOf('\\n');\n\twhile (newlineIndex !== UNFOUND_INDEX) {\n\t\tonLine(buffered.slice(0, newlineIndex).replace(/\\r$/u, ''));\n\t\tbuffered = buffered.slice(newlineIndex + 1);\n\t\tnewlineIndex = buffered.indexOf('\\n');\n\t}\n\n\treturn buffered;\n};\n\nconst decodeLogStream = () => {\n\tconst decoder = new TextDecoder();\n\n\treturn new TransformStream<Uint8Array, string>({\n\t\tflush: (controller) => {\n\t\t\tconst remaining = decoder.decode();\n\t\t\tif (remaining) controller.enqueue(remaining);\n\t\t},\n\t\ttransform: (chunk, controller) => {\n\t\t\tconst decoded = decoder.decode(chunk, { stream: true });\n\t\t\tif (decoded) controller.enqueue(decoded);\n\t\t}\n\t});\n};\n\nexport const isIgnorableAbsoluteAndroidLog = (\n\tentry: AbsoluteAndroidNativeLogEntry\n) =>\n\tentry.tag === 'Capacitor/Console' &&\n\tentry.message.includes('Error injecting safe area CSS:') &&\n\tentry.message.includes(\"Cannot read properties of null (reading 'style')\");\nexport const parseAbsoluteAndroidLogLine = (\n\tline: string\n): AbsoluteAndroidNativeLogEntry | null => {\n\tconst sanitized = redactAbsoluteAndroidLog(line).trim();\n\tif (!sanitized) return null;\n\tconst match = LOGCAT_LINE_PATTERN.exec(sanitized);\n\tif (!match) {\n\t\treturn { level: 'info', message: sanitized, tag: 'logcat' };\n\t}\n\n\treturn {\n\t\tlevel: androidLogLevel(match[1] ?? 'I'),\n\t\tmessage: match[LOGCAT_MESSAGE_CAPTURE] ?? '',\n\t\ttag: match[2]?.trim() || 'android'\n\t};\n};\n\nconst consumeLogLines = async (\n\tstream: ReadableStream<Uint8Array>,\n\tonLine: (line: string) => void\n) => {\n\tlet buffered = '';\n\tawait stream.pipeThrough(decodeLogStream()).pipeTo(\n\t\tnew WritableStream({\n\t\t\twrite: (chunk) => {\n\t\t\t\tbuffered = emitCompleteLogLines(buffered + chunk, onLine);\n\t\t\t}\n\t\t})\n\t);\n\tif (buffered) {\n\t\tonLine(buffered.replace(/\\r$/u, ''));\n\t}\n};\n\nconst startNativeLogStream = (\n\tcommand: string[],\n\toptions: AbsoluteAndroidCommandOptions,\n\tonLine: (line: string) => void\n): AbsoluteAndroidLogStream => {\n\tconst subprocess = Bun.spawn(command, {\n\t\tenv: options.env,\n\t\tstderr: 'pipe',\n\t\tstdin: 'ignore',\n\t\tstdout: 'pipe'\n\t});\n\tconst output = Promise.all([\n\t\tconsumeLogLines(subprocess.stdout, onLine),\n\t\tconsumeLogLines(subprocess.stderr, onLine)\n\t]);\n\tvoid output.catch(() => undefined);\n\n\treturn {\n\t\tclose: async () => {\n\t\t\ttry {\n\t\t\t\tsubprocess.kill();\n\t\t\t} catch {\n\t\t\t\t/* logcat already exited */\n\t\t\t}\n\t\t\tawait Promise.all([\n\t\t\t\tsubprocess.exited.catch(() => undefined),\n\t\t\t\toutput.catch(() => undefined)\n\t\t\t]);\n\t\t}\n\t};\n};\n\nconst requireSuccess = async (\n\tcommand: string[],\n\tlabel: string,\n\trun: NonNullable<StartAbsoluteAndroidDevOptions['run']>,\n\toptions?: AbsoluteAndroidCommandOptions\n) => {\n\tconst exitCode = await run(command, options);\n\tthrowIfAborted(options?.signal);\n\tif (exitCode !== 0) throw new Error(`${label} failed (exit ${exitCode}).`);\n};\n\nconst throwIfAborted = (signal?: AbortSignal) => {\n\tif (!signal?.aborted) return;\n\tthrow new DOMException(\n\t\t'Android development startup was cancelled.',\n\t\t'AbortError'\n\t);\n};\n\nconst journalPaths = (projectRoot: string) => {\n\tconst root = join(projectRoot, '.absolutejs', 'mobile', 'dev-session');\n\n\treturn {\n\t\tbackup: join(root, 'capacitor.config.backup.json'),\n\t\tcaBackup: join(root, 'absolutejs_dev_ca.backup.pem'),\n\t\tjournal: join(root, 'journal.json'),\n\t\tmanifestBackup: join(root, 'AndroidManifest.backup.xml'),\n\t\tnetworkConfigBackup: join(\n\t\t\troot,\n\t\t\t'absolutejs_dev_network_security.backup.xml'\n\t\t),\n\t\troot\n\t};\n};\n\nconst isInside = (root: string, path: string) => {\n\tconst resolvedRoot = resolve(root);\n\tconst resolvedPath = resolve(path);\n\tconst relativePath = relative(resolvedRoot, resolvedPath);\n\n\treturn (\n\t\trelativePath === '' ||\n\t\t(relativePath !== '..' &&\n\t\t\t!relativePath.startsWith(`..${sep}`) &&\n\t\t\t!isAbsolute(relativePath))\n\t);\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst nativeCachePath = (projectRoot: string) =>\n\tjoin(projectRoot, '.absolutejs', 'mobile', 'cache', 'android-debug.json');\n\nconst parseNativeCache = (value: unknown): AndroidNativeCache | null => {\n\tif (!isRecord(value)) return null;\n\tconst { appId, fingerprint, format, installations } = value;\n\tif (\n\t\tformat !== NATIVE_CACHE_FORMAT ||\n\t\ttypeof appId !== 'string' ||\n\t\ttypeof fingerprint !== 'string' ||\n\t\t!isRecord(installations) ||\n\t\t!Object.values(installations).every(\n\t\t\t(identity) => typeof identity === 'string'\n\t\t)\n\t) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tappId,\n\t\tfingerprint,\n\t\tformat,\n\t\tinstallations: Object.fromEntries(\n\t\t\tObject.entries(installations).map(([serial, identity]) => [\n\t\t\t\tserial,\n\t\t\t\tString(identity)\n\t\t\t])\n\t\t)\n\t};\n};\n\nconst readNativeCache = async (projectRoot: string) =>\n\treadFile(nativeCachePath(projectRoot), 'utf8')\n\t\t.then((source) => parseNativeCache(JSON.parse(source)))\n\t\t.catch(() => null);\n\nconst writeNativeCache = async (\n\tprojectRoot: string,\n\tcache: AndroidNativeCache\n) => {\n\tconst destination = nativeCachePath(projectRoot);\n\tconst temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;\n\tawait mkdir(dirname(destination), { recursive: true });\n\ttry {\n\t\tawait writeFile(temporary, `${JSON.stringify(cache, null, '\\t')}\\n`, {\n\t\t\tflag: 'wx'\n\t\t});\n\t\tawait rename(temporary, destination);\n\t} finally {\n\t\tawait rm(temporary, { force: true }).catch(() => undefined);\n\t}\n};\n\nconst nativeDependencySources = async (nativeDirectory: string) => {\n\tconst settings = await readFile(\n\t\tjoin(nativeDirectory, 'capacitor.settings.gradle'),\n\t\t'utf8'\n\t);\n\tconst pattern = new RegExp(\n\t\tCAPACITOR_PROJECT_DIRECTORY_PATTERN.source,\n\t\tCAPACITOR_PROJECT_DIRECTORY_PATTERN.flags\n\t);\n\tconst dependencies = [...settings.matchAll(pattern)].map((match) => ({\n\t\tname: (match[1] ?? '').slice(1).replaceAll(/[^a-zA-Z0-9_.-]/gu, '_'),\n\t\tsource: resolve(nativeDirectory, match[2] ?? '')\n\t}));\n\tif (dependencies.length === 0) {\n\t\tthrow new Error(\n\t\t\t'Capacitor Android settings did not declare any native dependencies.'\n\t\t);\n\t}\n\n\treturn { dependencies, settings };\n};\n\nconst shouldIgnoreNativePath = (\n\trelativePath: string,\n\tignorePublicBundle: boolean\n) => {\n\tconst parts = relativePath.split(sep);\n\tif (\n\t\tparts.includes('.gradle') ||\n\t\tparts.includes('build') ||\n\t\tparts.includes('.absolutejs-dependencies')\n\t) {\n\t\treturn true;\n\t}\n\n\treturn (\n\t\tignorePublicBundle &&\n\t\tparts.slice(0, NATIVE_PUBLIC_PATH_SEGMENTS).join('/') ===\n\t\t\t'app/src/main/assets/public'\n\t);\n};\n\ntype NativeDirectoryCollector = (\n\troot: string,\n\tlabel: string,\n\tdirectory: string,\n\tignorePublicBundle: boolean\n) => Promise<string[]>;\n\nconst collectNativePath = async (\n\troot: string,\n\tlabel: string,\n\tpath: string,\n\tisDirectory: boolean,\n\tisFile: boolean,\n\tisSymbolicLink: boolean,\n\tignorePublicBundle: boolean\n) => {\n\tconst relativePath = relative(root, path);\n\tif (shouldIgnoreNativePath(relativePath, ignorePublicBundle)) return [];\n\tconst identity = `${label}:${relativePath.split(sep).join('/')}\\0`;\n\tif (isDirectory) {\n\t\treturn collectNativeDirectory(root, label, path, ignorePublicBundle);\n\t}\n\tif (isSymbolicLink) {\n\t\treturn [`link\\0${identity}${await readlink(path)}\\0`];\n\t}\n\tif (!isFile) return [];\n\tconst [metadata, contents] = await Promise.all([\n\t\tlstat(path),\n\t\treadFile(path)\n\t]);\n\tconst contentDigest = createHash('sha256').update(contents).digest('hex');\n\n\treturn [\n\t\t`file\\0${identity}${metadata.mode & EXECUTABLE_MODE_MASK}\\0${contentDigest}\\0`\n\t];\n};\n\nconst collectNativeDirectory: NativeDirectoryCollector = async (\n\troot,\n\tlabel,\n\tdirectory,\n\tignorePublicBundle\n) => {\n\tconst entries = await readdir(directory, { withFileTypes: true });\n\tentries.sort((left, right) => left.name.localeCompare(right.name));\n\tconst records = await Promise.all(\n\t\tentries.map((entry) =>\n\t\t\tcollectNativePath(\n\t\t\t\troot,\n\t\t\t\tlabel,\n\t\t\t\tjoin(directory, entry.name),\n\t\t\t\tentry.isDirectory(),\n\t\t\t\tentry.isFile(),\n\t\t\t\tentry.isSymbolicLink(),\n\t\t\t\tignorePublicBundle\n\t\t\t)\n\t\t)\n\t);\n\n\treturn records.flat();\n};\n\nconst hashNativeTree = async (\n\troot: string,\n\tlabel: string,\n\tignorePublicBundle: boolean\n) => {\n\tconst resolvedRoot = await realpath(root);\n\tconst records = await collectNativeDirectory(\n\t\tresolvedRoot,\n\t\tlabel,\n\t\tresolvedRoot,\n\t\tignorePublicBundle\n\t);\n\n\treturn createHash('sha256').update(records.join('')).digest('hex');\n};\n\ntype NativeFingerprintRoot = {\n\tignorePublicBundle: boolean;\n\tlabel: string;\n\tsource: string;\n};\n\nexport const fingerprintAbsoluteAndroidNativeProject = async (\n\tproject: Pick<AbsoluteAndroidDevProject, 'nativeDirectory'>,\n\toptions: { includePublicBundle?: boolean } = {}\n) => {\n\tconst { dependencies } = await nativeDependencySources(\n\t\tproject.nativeDirectory\n\t);\n\tconst roots: NativeFingerprintRoot[] = [\n\t\t{\n\t\t\tignorePublicBundle: options.includePublicBundle !== true,\n\t\t\tlabel: 'android',\n\t\t\tsource: project.nativeDirectory\n\t\t},\n\t\t...dependencies\n\t\t\t.sort((left, right) => left.name.localeCompare(right.name))\n\t\t\t.map((dependency) => ({\n\t\t\t\tignorePublicBundle: false,\n\t\t\t\tlabel: `dependency:${dependency.name}`,\n\t\t\t\tsource: dependency.source\n\t\t\t}))\n\t];\n\tconst treeDigests = await Promise.all(\n\t\troots.map((root) =>\n\t\t\thashNativeTree(root.source, root.label, root.ignorePublicBundle)\n\t\t)\n\t);\n\n\treturn createHash('sha256').update(treeDigests.join('\\0')).digest('hex');\n};\n\nconst parseJournal = (value: unknown): AndroidDevJournal | null => {\n\tif (typeof value !== 'object' || value === null) return null;\n\tconst backupPath = Reflect.get(value, 'backupPath');\n\tconst format = Reflect.get(value, 'format');\n\tconst manifestBackupPath = Reflect.get(value, 'manifestBackupPath');\n\tconst nativeConfigPath = Reflect.get(value, 'nativeConfigPath');\n\tconst nativeManifestPath = Reflect.get(value, 'nativeManifestPath');\n\tconst projectedFiles = Reflect.get(value, 'projectedFiles');\n\tif (\n\t\ttypeof backupPath !== 'string' ||\n\t\tformat !== DEV_JOURNAL_FORMAT ||\n\t\ttypeof nativeConfigPath !== 'string'\n\t) {\n\t\treturn null;\n\t}\n\tif (\n\t\t(manifestBackupPath !== undefined ||\n\t\t\tnativeManifestPath !== undefined) &&\n\t\t(typeof manifestBackupPath !== 'string' ||\n\t\t\ttypeof nativeManifestPath !== 'string')\n\t) {\n\t\treturn null;\n\t}\n\tif (\n\t\tprojectedFiles !== undefined &&\n\t\t(!Array.isArray(projectedFiles) ||\n\t\t\t!projectedFiles.every(\n\t\t\t\t(file) =>\n\t\t\t\t\tisRecord(file) &&\n\t\t\t\t\ttypeof file.path === 'string' &&\n\t\t\t\t\t(file.backupPath === undefined ||\n\t\t\t\t\t\ttypeof file.backupPath === 'string')\n\t\t\t))\n\t) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tbackupPath,\n\t\tformat,\n\t\tmanifestBackupPath,\n\t\tnativeConfigPath,\n\t\tnativeManifestPath,\n\t\tprojectedFiles\n\t};\n};\n\nconst restoreAndroidProjectedFile = async (\n\tfile: NonNullable<AndroidDevJournal['projectedFiles']>[number]\n) => {\n\tif (!file.backupPath || !(await pathExists(file.backupPath))) {\n\t\tawait rm(file.path, { force: true });\n\n\t\treturn;\n\t}\n\tawait mkdir(dirname(file.path), { recursive: true });\n\tawait copyFile(file.backupPath, file.path);\n};\n\nexport const repairAbsoluteAndroidDevSession = async (projectRoot: string) => {\n\tconst paths = journalPaths(projectRoot);\n\tif (!(await pathExists(paths.journal))) {\n\t\tawait rm(paths.root, { force: true, recursive: true });\n\n\t\treturn false;\n\t}\n\tconst journal = await readFile(paths.journal, 'utf8')\n\t\t.then((source) => parseJournal(JSON.parse(source)))\n\t\t.catch(() => null);\n\tif (\n\t\t!journal ||\n\t\t!isInside(projectRoot, journal.nativeConfigPath) ||\n\t\t!isInside(paths.root, journal.backupPath) ||\n\t\t(journal.nativeManifestPath !== undefined &&\n\t\t\t!isInside(projectRoot, journal.nativeManifestPath)) ||\n\t\t(journal.manifestBackupPath !== undefined &&\n\t\t\t!isInside(paths.root, journal.manifestBackupPath)) ||\n\t\tjournal.projectedFiles?.some(\n\t\t\t(file) =>\n\t\t\t\t!isInside(projectRoot, file.path) ||\n\t\t\t\t(file.backupPath !== undefined &&\n\t\t\t\t\t!isInside(paths.root, file.backupPath))\n\t\t)\n\t) {\n\t\tthrow new Error(\n\t\t\t`Refusing unsafe or invalid mobile dev journal at ${paths.journal}.`\n\t\t);\n\t}\n\tif (await pathExists(journal.backupPath)) {\n\t\tawait mkdir(dirname(journal.nativeConfigPath), { recursive: true });\n\t\tawait copyFile(journal.backupPath, journal.nativeConfigPath);\n\t}\n\tif (\n\t\tjournal.manifestBackupPath &&\n\t\tjournal.nativeManifestPath &&\n\t\t(await pathExists(journal.manifestBackupPath))\n\t) {\n\t\tawait mkdir(dirname(journal.nativeManifestPath), { recursive: true });\n\t\tawait copyFile(journal.manifestBackupPath, journal.nativeManifestPath);\n\t}\n\tawait Promise.all(\n\t\t(journal.projectedFiles ?? []).map(restoreAndroidProjectedFile)\n\t);\n\tawait rm(paths.root, { force: true, recursive: true });\n\n\treturn true;\n};\n\nconst setAndroidApplicationAttribute = (\n\tsource: string,\n\tattribute: string,\n\tvalue: string\n) => {\n\tconst pattern = new RegExp(`android:${attribute}=[\"'][^\"']*[\"']`, 'u');\n\tif (pattern.test(source))\n\t\treturn source.replace(pattern, `android:${attribute}=\"${value}\"`);\n\n\treturn source.replace(\n\t\t'<application',\n\t\t`<application\\n android:${attribute}=\"${value}\"`\n\t);\n};\n\nconst androidDevelopmentManifest = (\n\tsource: string,\n\tcleartext: boolean,\n\tdevelopmentNetworkConfig?: string\n) => {\n\tlet projected = source;\n\tif (developmentNetworkConfig) {\n\t\tprojected = setAndroidApplicationAttribute(\n\t\t\tprojected,\n\t\t\t'networkSecurityConfig',\n\t\t\t`@xml/${developmentNetworkConfig}`\n\t\t);\n\t}\n\tif (!cleartext) return projected;\n\tif (/android:usesCleartextTraffic=[\"'][^\"']*[\"']/u.test(source)) {\n\t\treturn projected.replace(\n\t\t\t/android:usesCleartextTraffic=[\"'][^\"']*[\"']/u,\n\t\t\t'android:usesCleartextTraffic=\"true\"'\n\t\t);\n\t}\n\n\treturn setAndroidApplicationAttribute(\n\t\tprojected,\n\t\t'usesCleartextTraffic',\n\t\t'true'\n\t);\n};\n\nconst withAndroidDebugCertificateAuthority = (source: string) => {\n\tif (source.includes('@raw/absolutejs_dev_ca')) return source;\n\tconst debugOverrides =\n\t\t/<debug-overrides(?:\\s[^>]*)?>([\\s\\S]*?)<\\/debug-overrides>/u;\n\tconst match = source.match(debugOverrides);\n\tif (match) {\n\t\tconst [block] = match;\n\t\tconst trustAnchors = /<trust-anchors(?:\\s[^>]*)?>/u;\n\t\tconst projectedBlock = trustAnchors.test(block)\n\t\t\t? block.replace(\n\t\t\t\t\ttrustAnchors,\n\t\t\t\t\t'$&\\n\\t\\t\\t<certificates src=\"@raw/absolutejs_dev_ca\" />'\n\t\t\t\t)\n\t\t\t: block.replace(\n\t\t\t\t\t'</debug-overrides>',\n\t\t\t\t\t'\\t<trust-anchors>\\n\\t\\t\\t<certificates src=\"@raw/absolutejs_dev_ca\" />\\n\\t\\t</trust-anchors>\\n\\t</debug-overrides>'\n\t\t\t\t);\n\n\t\treturn source.replace(block, projectedBlock);\n\t}\n\tif (!source.includes('</network-security-config>')) {\n\t\tthrow new Error('Android Network Security Configuration is invalid.');\n\t}\n\n\treturn source.replace(\n\t\t'</network-security-config>',\n\t\t'\\t<debug-overrides>\\n\\t\\t<trust-anchors>\\n\\t\\t\\t<certificates src=\"@raw/absolutejs_dev_ca\" />\\n\\t\\t</trust-anchors>\\n\\t</debug-overrides>\\n</network-security-config>'\n\t);\n};\n\nconst writeDevConfig = async (\n\tprojectRoot: string,\n\tnativeConfigPath: string,\n\tnativeManifestPath: string,\n\tport: number,\n\thttps: boolean,\n\tentry: string,\n\tembeddedBundle: boolean,\n\tserverHost: string,\n\tcertificateAuthorityPath?: string\n) => {\n\tconst paths = journalPaths(projectRoot);\n\tawait repairAbsoluteAndroidDevSession(projectRoot);\n\tconst source = await readFile(nativeConfigPath, 'utf8');\n\tconst manifestSource = await readFile(nativeManifestPath, 'utf8');\n\tconst parsed: unknown = JSON.parse(source);\n\tif (!isRecord(parsed)) {\n\t\tthrow new Error(\n\t\t\t`Invalid Capacitor native config at ${nativeConfigPath}.`\n\t\t);\n\t}\n\tawait mkdir(paths.root, { recursive: true });\n\tawait writeFile(paths.backup, source, { flag: 'wx' });\n\tawait writeFile(paths.manifestBackup, manifestSource, { flag: 'wx' });\n\tconst resourceRoot = join(dirname(nativeManifestPath), 'res');\n\tconst caPath = join(resourceRoot, 'raw', 'absolutejs_dev_ca.pem');\n\tconst existingNetworkConfig = manifestSource.match(\n\t\t/android:networkSecurityConfig=[\"']@xml\\/([a-z0-9_]+)[\"']/u\n\t)?.[1];\n\tconst networkConfigName =\n\t\texistingNetworkConfig ?? 'absolutejs_dev_network_security';\n\tconst networkConfigPath = join(\n\t\tresourceRoot,\n\t\t'xml',\n\t\t`${networkConfigName}.xml`\n\t);\n\tconst backupProjectedFile = async ([path, backupPath]: readonly [\n\t\tstring,\n\t\tstring\n\t]) => {\n\t\tif (!(await pathExists(path))) return { path };\n\t\tawait copyFile(path, backupPath);\n\n\t\treturn { backupPath, path };\n\t};\n\tconst projectedFiles: AndroidDevJournal['projectedFiles'] =\n\t\thttps && certificateAuthorityPath\n\t\t\t? await Promise.all(\n\t\t\t\t\t(\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t[caPath, paths.caBackup],\n\t\t\t\t\t\t\t[networkConfigPath, paths.networkConfigBackup]\n\t\t\t\t\t\t] as const\n\t\t\t\t\t).map(backupProjectedFile)\n\t\t\t\t)\n\t\t\t: [];\n\tconst journal: AndroidDevJournal = {\n\t\tbackupPath: paths.backup,\n\t\tformat: DEV_JOURNAL_FORMAT,\n\t\tmanifestBackupPath: paths.manifestBackup,\n\t\tnativeConfigPath,\n\t\tnativeManifestPath,\n\t\tprojectedFiles\n\t};\n\tawait writeFile(paths.journal, `${JSON.stringify(journal, null, '\\t')}\\n`, {\n\t\tflag: 'wx'\n\t});\n\tif (!embeddedBundle) {\n\t\tconst currentServer = parsed.server;\n\t\tconst developmentUrl = new URL(\n\t\t\t`${https ? 'https' : 'http'}://${serverHost}:${port}${entry}`\n\t\t);\n\t\tdevelopmentUrl.searchParams.set(\n\t\t\t'__absolute_target',\n\t\t\t'capacitor-android'\n\t\t);\n\t\tparsed.server = {\n\t\t\t...(typeof currentServer === 'object' && currentServer !== null\n\t\t\t\t? currentServer\n\t\t\t\t: {}),\n\t\t\tcleartext: !https,\n\t\t\turl: developmentUrl.href\n\t\t};\n\t}\n\tawait writeFile(\n\t\tnativeConfigPath,\n\t\t`${JSON.stringify(parsed, null, '\\t')}\\n`\n\t);\n\tif (https && certificateAuthorityPath) {\n\t\tawait Promise.all([\n\t\t\tmkdir(dirname(caPath), { recursive: true }),\n\t\t\tmkdir(dirname(networkConfigPath), { recursive: true })\n\t\t]);\n\t\tawait copyFile(certificateAuthorityPath, caPath);\n\t\tconst existingNetworkConfigSource = existingNetworkConfig\n\t\t\t? await readFile(networkConfigPath, 'utf8')\n\t\t\t: '<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n<network-security-config>\\n</network-security-config>\\n';\n\t\tawait writeFile(\n\t\t\tnetworkConfigPath,\n\t\t\twithAndroidDebugCertificateAuthority(existingNetworkConfigSource)\n\t\t);\n\t}\n\tawait writeFile(\n\t\tnativeManifestPath,\n\t\tandroidDevelopmentManifest(\n\t\t\tmanifestSource,\n\t\t\t!https,\n\t\t\thttps && certificateAuthorityPath !== undefined\n\t\t\t\t? networkConfigName\n\t\t\t\t: undefined\n\t\t)\n\t);\n};\n\nexport const parseAdbDevices = (output: string) =>\n\toutput\n\t\t.split(/\\r?\\n/)\n\t\t.slice(1)\n\t\t.map((line) => line.trim().split(/\\s+/, 2))\n\t\t.filter(\n\t\t\t(parts): parts is [string, string] =>\n\t\t\t\tparts.length === 2 && parts[1] === 'device'\n\t\t)\n\t\t.map(([serial]) => serial);\n\nconst isManagedEmulatorSerial = (\n\tserial: string,\n\tadb: string,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tif (!serial.startsWith('emulator-')) return false;\n\tconst avd = capture([adb, '-s', serial, 'emu', 'avd', 'name'], { env });\n\n\treturn (\n\t\tavd.exitCode === 0 &&\n\t\tavd.stdout\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((value) => value.trim())\n\t\t\t.includes(ABSOLUTE_ANDROID_AVD_NAME)\n\t);\n};\n\nconst managedEmulatorSerial = (\n\tadb: string,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tconst devices = capture([adb, 'devices'], { env });\n\tif (devices.exitCode !== 0) return undefined;\n\n\treturn parseAdbDevices(devices.stdout).find((serial) =>\n\t\tisManagedEmulatorSerial(serial, adb, capture, env)\n\t);\n};\n\nconst requireConnectedAndroidDevice = (\n\tadb: string,\n\tserial: string,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tconst devices = capture([adb, 'devices'], { env });\n\tif (\n\t\tdevices.exitCode !== 0 ||\n\t\t!parseAdbDevices(devices.stdout).includes(serial)\n\t) {\n\t\tthrow new Error(\n\t\t\t`Android device ${serial} is not connected and authorized. Confirm the USB debugging prompt, then retry.`\n\t\t);\n\t}\n};\n\nconst completedBootSerial = (\n\tproject: AbsoluteAndroidDevProject,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tconst serial = managedEmulatorSerial(project.adb, capture, env);\n\tif (!serial) return undefined;\n\tconst booted = capture(\n\t\t[project.adb, '-s', serial, 'shell', 'getprop', 'sys.boot_completed'],\n\t\t{ env }\n\t);\n\n\treturn booted.exitCode === 0 && booted.stdout.trim() === '1'\n\t\t? serial\n\t\t: undefined;\n};\n\nconst preferredCompletedBootSerial = (\n\tproject: AbsoluteAndroidDevProject,\n\tserial: string,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tconst booted = capture(\n\t\t[project.adb, '-s', serial, 'shell', 'getprop', 'sys.boot_completed'],\n\t\t{ env }\n\t);\n\n\treturn booted.exitCode === 0 && booted.stdout.trim() === '1'\n\t\t? serial\n\t\t: undefined;\n};\n\nconst waitForManagedEmulator = async (\n\tproject: AbsoluteAndroidDevProject,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tsleep: NonNullable<StartAbsoluteAndroidDevOptions['sleep']>,\n\tenv: Record<string, string | undefined>,\n\tsignal?: AbortSignal,\n\tpreferredSerial?: string\n) => {\n\tconst deadline = Date.now() + ANDROID_BOOT_TIMEOUT_MS;\n\tconst poll = async () => {\n\t\tthrowIfAborted(signal);\n\t\tconst serial = preferredSerial\n\t\t\t? preferredCompletedBootSerial(\n\t\t\t\t\tproject,\n\t\t\t\t\tpreferredSerial,\n\t\t\t\t\tcapture,\n\t\t\t\t\tenv\n\t\t\t\t)\n\t\t\t: completedBootSerial(project, capture, env);\n\t\tif (serial) return serial;\n\t\tif (Date.now() >= deadline) {\n\t\t\tthrow new Error(\n\t\t\t\t`Android ${preferredSerial ? `target ${preferredSerial}` : `emulator ${ABSOLUTE_ANDROID_AVD_NAME}`} did not finish booting within ${ANDROID_BOOT_TIMEOUT_MS / ANDROID_BOOT_POLL_MS}s.`\n\t\t\t);\n\t\t}\n\t\tawait sleep(ANDROID_BOOT_POLL_MS);\n\n\t\treturn poll();\n\t};\n\n\treturn poll();\n};\n\nconst windowsPathFromWsl = (\n\tpath: string,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>\n) => {\n\tconst result = capture(['wslpath', '-w', path]);\n\tif (result.exitCode !== 0 || !result.stdout.trim()) {\n\t\tthrow new Error(`Could not translate WSL path for Windows: ${path}`);\n\t}\n\n\treturn result.stdout.trim();\n};\n\ntype WindowsGradleDependency = {\n\tname: string;\n\twindowsSource: string;\n};\n\nconst mirroredCapacitorDependencies = async (\n\tproject: Pick<AbsoluteAndroidDevProject, 'nativeDirectory'>,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>\n) => {\n\tconst settingsPath = join(\n\t\tproject.nativeDirectory,\n\t\t'capacitor.settings.gradle'\n\t);\n\tconst settings = await readFile(settingsPath, 'utf8');\n\tconst dependencies: WindowsGradleDependency[] = [];\n\tconst rewrittenSettings = settings.replace(\n\t\tCAPACITOR_PROJECT_DIRECTORY_PATTERN,\n\t\t(_statement, projectName: string, sourcePath: string) => {\n\t\t\tconst name = projectName\n\t\t\t\t.slice(1)\n\t\t\t\t.replaceAll(/[^a-zA-Z0-9_.-]/gu, '_');\n\t\t\tconst source = resolve(project.nativeDirectory, sourcePath);\n\t\t\tdependencies.push({\n\t\t\t\tname,\n\t\t\t\twindowsSource: windowsPathFromWsl(source, capture)\n\t\t\t});\n\n\t\t\treturn `project('${projectName}').projectDir = new File('./.absolutejs-dependencies/${name}')`;\n\t\t}\n\t);\n\tif (dependencies.length === 0) {\n\t\tthrow new Error(\n\t\t\t'Capacitor Android settings did not declare any native dependencies.'\n\t\t);\n\t}\n\n\treturn { dependencies, rewrittenSettings };\n};\n\nconst encodedWindowsGradleCommand = (\n\twindowsSource: string,\n\twindowsDirectory: string,\n\twindowsAndroidRoot: string,\n\tdependencies: WindowsGradleDependency[],\n\trewrittenSettings: string,\n\ttask: AbsoluteAndroidGradleTask,\n\tgradleArguments: readonly string[]\n) => {\n\tconst sourceDirectory = Buffer.from(windowsSource, 'utf8').toString(\n\t\t'base64'\n\t);\n\tconst buildDirectory = Buffer.from(windowsDirectory, 'utf8').toString(\n\t\t'base64'\n\t);\n\tconst androidRoot = Buffer.from(windowsAndroidRoot, 'utf8').toString(\n\t\t'base64'\n\t);\n\tconst dependencyData = Buffer.from(\n\t\tJSON.stringify(dependencies),\n\t\t'utf8'\n\t).toString('base64');\n\tconst settingsData = Buffer.from(rewrittenSettings, 'utf8').toString(\n\t\t'base64'\n\t);\n\tconst argumentsData = Buffer.from(\n\t\tJSON.stringify(gradleArguments),\n\t\t'utf8'\n\t).toString('base64');\n\tconst source = [\n\t\t\"$ErrorActionPreference = 'Stop'\",\n\t\t`$source = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${sourceDirectory}'))`,\n\t\t`$directory = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${buildDirectory}'))`,\n\t\t`$androidHome = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${androidRoot}'))`,\n\t\t`$dependencies = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${dependencyData}')) | ConvertFrom-Json`,\n\t\t`$settings = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${settingsData}'))`,\n\t\t`$gradleArguments = @([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${argumentsData}')) | ConvertFrom-Json)`,\n\t\t'$env:ANDROID_HOME = $androidHome',\n\t\t'$env:ANDROID_SDK_ROOT = $androidHome',\n\t\t'New-Item -ItemType Directory -Force -Path $directory | Out-Null',\n\t\t'& robocopy.exe $source $directory /MIR /XD .gradle build .absolutejs-dependencies /NFL /NDL /NJH /NJS /NP',\n\t\t'$copyExit = $LASTEXITCODE',\n\t\t'if ($copyExit -ge 8) { exit $copyExit }',\n\t\t\"foreach ($dependency in @($dependencies)) { $target = Join-Path $directory ('.absolutejs-dependencies\\\\' + $dependency.name); New-Item -ItemType Directory -Force -Path $target | Out-Null; & robocopy.exe $dependency.windowsSource $target /MIR /XD .gradle build /NFL /NDL /NJH /NJS /NP; if ($LASTEXITCODE -ge 8) { exit $LASTEXITCODE } }\",\n\t\t\"[IO.File]::WriteAllText((Join-Path $directory 'capacitor.settings.gradle'), $settings)\",\n\t\t\"$wrapper = Join-Path $directory 'gradlew.bat'\",\n\t\t`& $wrapper --no-daemon --console=plain -p $directory @gradleArguments ${task}`,\n\t\t'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }',\n\t\t'exit 0'\n\t].join('; ');\n\n\treturn Buffer.from(source, 'utf16le').toString('base64');\n};\n\nexport type AbsoluteAndroidGradleTask =\n\t'assembleDebug' | 'assembleRelease' | 'bundleRelease';\n\nexport type BuildAbsoluteAndroidGradleOptions = {\n\tcapture?: NonNullable<StartAbsoluteAndroidDevOptions['capture']>;\n\tenv?: Record<string, string | undefined>;\n\tgradleArguments?: readonly string[];\n\tproject: Pick<\n\t\tAbsoluteAndroidDevProject,\n\t\t'androidRoot' | 'config' | 'host' | 'nativeDirectory' | 'projectRoot'\n\t>;\n\trun?: NonNullable<StartAbsoluteAndroidDevOptions['run']>;\n\tsignal?: AbortSignal;\n\ttask: AbsoluteAndroidGradleTask;\n};\n\nconst gradleArtifactPath = (\n\tnativeDirectory: string,\n\ttask: AbsoluteAndroidGradleTask,\n\twindows = false\n) => {\n\tconst pathJoin = windows ? win32.join : join;\n\tif (task === 'assembleDebug') {\n\t\treturn pathJoin(\n\t\t\tnativeDirectory,\n\t\t\t'app',\n\t\t\t'build',\n\t\t\t'outputs',\n\t\t\t'apk',\n\t\t\t'debug',\n\t\t\t'app-debug.apk'\n\t\t);\n\t}\n\tif (task === 'assembleRelease') {\n\t\treturn pathJoin(\n\t\t\tnativeDirectory,\n\t\t\t'app',\n\t\t\t'build',\n\t\t\t'outputs',\n\t\t\t'apk',\n\t\t\t'release',\n\t\t\t'app-release.apk'\n\t\t);\n\t}\n\n\treturn pathJoin(\n\t\tnativeDirectory,\n\t\t'app',\n\t\t'build',\n\t\t'outputs',\n\t\t'bundle',\n\t\t'release',\n\t\t'app-release.aab'\n\t);\n};\n\nconst resolveGradleArtifactPath = async (\n\tnativeDirectory: string,\n\ttask: AbsoluteAndroidGradleTask\n) => {\n\tconst primary = gradleArtifactPath(nativeDirectory, task);\n\tif (task !== 'assembleRelease' || (await pathExists(primary))) {\n\t\treturn primary;\n\t}\n\n\treturn join(\n\t\tnativeDirectory,\n\t\t'app',\n\t\t'build',\n\t\t'outputs',\n\t\t'apk',\n\t\t'release',\n\t\t'app-release-unsigned.apk'\n\t);\n};\n\nexport const buildAbsoluteAndroidGradleArtifact = async (\n\toptions: BuildAbsoluteAndroidGradleOptions\n) => {\n\tconst { project, task } = options;\n\tconst capture = options.capture ?? captureCommand;\n\tconst run = options.run ?? runCommand;\n\tconst env = options.env ?? process.env;\n\tconst gradleArguments = options.gradleArguments ?? [];\n\tif (project.host === 'wsl') {\n\t\tconst windowsSource = windowsPathFromWsl(\n\t\t\tproject.nativeDirectory,\n\t\t\tcapture\n\t\t);\n\t\tconst buildId = Bun.hash(project.projectRoot).toString(HASH_RADIX);\n\t\tconst managedBuildDirectory = resolve(\n\t\t\tproject.androidRoot,\n\t\t\t'..',\n\t\t\t'..',\n\t\t\t'Builds',\n\t\t\t`${project.config.appId}-${buildId}`\n\t\t);\n\t\tconst windowsDirectory = windowsPathFromWsl(\n\t\t\tmanagedBuildDirectory,\n\t\t\tcapture\n\t\t);\n\t\tconst windowsAndroidRoot = windowsPathFromWsl(\n\t\t\tproject.androidRoot,\n\t\t\tcapture\n\t\t);\n\t\tconst { dependencies, rewrittenSettings } =\n\t\t\tawait mirroredCapacitorDependencies(project, capture);\n\t\tawait requireSuccess(\n\t\t\t[\n\t\t\t\t'powershell.exe',\n\t\t\t\t'-NoProfile',\n\t\t\t\t'-EncodedCommand',\n\t\t\t\tencodedWindowsGradleCommand(\n\t\t\t\t\twindowsSource,\n\t\t\t\t\twindowsDirectory,\n\t\t\t\t\twindowsAndroidRoot,\n\t\t\t\t\tdependencies,\n\t\t\t\t\trewrittenSettings,\n\t\t\t\t\ttask,\n\t\t\t\t\tgradleArguments\n\t\t\t\t)\n\t\t\t],\n\t\t\t'Android Gradle build',\n\t\t\trun,\n\t\t\t{ env, signal: options.signal }\n\t\t);\n\n\t\tconst artifactPath = await resolveGradleArtifactPath(\n\t\t\tmanagedBuildDirectory,\n\t\t\ttask\n\t\t);\n\t\tconst unsigned = artifactPath.endsWith('-unsigned.apk');\n\n\t\treturn {\n\t\t\tartifactPath,\n\t\t\tinstallPath: unsigned\n\t\t\t\t? win32.join(\n\t\t\t\t\t\twindowsDirectory,\n\t\t\t\t\t\t'app',\n\t\t\t\t\t\t'build',\n\t\t\t\t\t\t'outputs',\n\t\t\t\t\t\t'apk',\n\t\t\t\t\t\t'release',\n\t\t\t\t\t\t'app-release-unsigned.apk'\n\t\t\t\t\t)\n\t\t\t\t: gradleArtifactPath(windowsDirectory, task, true)\n\t\t};\n\t}\n\tconst wrapper = project.host === 'windows' ? 'gradlew.bat' : './gradlew';\n\tawait requireSuccess(\n\t\t[wrapper, '--no-daemon', '--console=plain', ...gradleArguments, task],\n\t\t'Android Gradle build',\n\t\trun,\n\t\t{ cwd: project.nativeDirectory, env, signal: options.signal }\n\t);\n\n\tconst artifactPath = await resolveGradleArtifactPath(\n\t\tproject.nativeDirectory,\n\t\ttask\n\t);\n\n\treturn { artifactPath, installPath: artifactPath };\n};\n\nconst buildAndroidDebugApp = async (\n\tproject: AbsoluteAndroidDevProject,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\trun: NonNullable<StartAbsoluteAndroidDevOptions['run']>,\n\tenv: Record<string, string | undefined>,\n\tsignal?: AbortSignal\n) =>\n\t(\n\t\tawait buildAbsoluteAndroidGradleArtifact({\n\t\t\tcapture,\n\t\t\tenv,\n\t\t\tproject,\n\t\t\trun,\n\t\t\tsignal,\n\t\t\ttask: 'assembleDebug'\n\t\t})\n\t).installPath;\n\ntype EnsureAndroidDebugAppOptions = {\n\tcache: AndroidNativeCache | null;\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>;\n\tenv: Record<string, string | undefined>;\n\tfingerprint: string;\n\tlog: NonNullable<StartAbsoluteAndroidDevOptions['log']>;\n\tproject: AbsoluteAndroidDevProject;\n\trun: NonNullable<StartAbsoluteAndroidDevOptions['run']>;\n\tserial: string;\n\tsignal?: AbortSignal;\n\ttransition: (state: AbsoluteAndroidDevState) => void;\n};\n\nconst persistInstalledAndroidNativeCache = async (\n\toptions: EnsureAndroidDebugAppOptions,\n\tinstalled: AndroidInstalledPackage | undefined\n) => {\n\tif (!installed) {\n\t\toptions.log(\n\t\t\t'Android package identity was unavailable; the next session will rebuild safely.'\n\t\t);\n\n\t\treturn;\n\t}\n\tconst installations =\n\t\toptions.cache?.appId === options.project.config.appId &&\n\t\toptions.cache.fingerprint === options.fingerprint\n\t\t\t? { ...options.cache.installations }\n\t\t\t: {};\n\tinstallations[options.serial] = installed.identity;\n\tawait writeNativeCache(options.project.projectRoot, {\n\t\tappId: options.project.config.appId,\n\t\tfingerprint: options.fingerprint,\n\t\tformat: NATIVE_CACHE_FORMAT,\n\t\tinstallations\n\t}).catch((error) => {\n\t\toptions.log(\n\t\t\t`Android native cache could not be saved: ${error instanceof Error ? error.message : String(error)}`\n\t\t);\n\t});\n};\n\nconst ensureAndroidDebugApp = async (options: EnsureAndroidDebugAppOptions) => {\n\tconst installed = androidInstalledPackageIdentity(\n\t\toptions.project,\n\t\toptions.serial,\n\t\toptions.capture,\n\t\toptions.env\n\t);\n\tconst cacheHit =\n\t\toptions.cache?.appId === options.project.config.appId &&\n\t\toptions.cache.fingerprint === options.fingerprint &&\n\t\tinstalled !== undefined &&\n\t\toptions.cache.installations[options.serial] === installed.identity;\n\tif (cacheHit) {\n\t\toptions.log(\n\t\t\t`Android native app is unchanged on ${options.serial}; skipped Gradle build and APK install.`\n\t\t);\n\n\t\treturn { nativeCacheHit: true, uid: installed.uid };\n\t}\n\toptions.log(\n\t\t'Android native inputs changed or the installed app is stale; rebuilding.'\n\t);\n\toptions.transition('building');\n\tconst installPath = await buildAndroidDebugApp(\n\t\toptions.project,\n\t\toptions.capture,\n\t\toptions.run,\n\t\toptions.env,\n\t\toptions.signal\n\t);\n\tthrowIfAborted(options.signal);\n\toptions.transition('installing');\n\tconst installCommand = [\n\t\toptions.project.adb,\n\t\t'-s',\n\t\toptions.serial,\n\t\t'install',\n\t\t'-r',\n\t\t'-d',\n\t\tinstallPath\n\t];\n\tlet installExitCode = await options.run(installCommand, {\n\t\tenv: options.env,\n\t\tsignal: options.signal\n\t});\n\tthrowIfAborted(options.signal);\n\tif (installExitCode !== 0) {\n\t\toptions.log(\n\t\t\t'Android app installation lost its ADB transport; reconnecting and retrying once.'\n\t\t);\n\t\tawait requireSuccess(\n\t\t\t[options.project.adb, '-s', options.serial, 'wait-for-device'],\n\t\t\t'Android device reconnection',\n\t\t\toptions.run,\n\t\t\t{ env: options.env, signal: options.signal }\n\t\t);\n\t\tinstallExitCode = await options.run(installCommand, {\n\t\t\tenv: options.env,\n\t\t\tsignal: options.signal\n\t\t});\n\t\tthrowIfAborted(options.signal);\n\t}\n\tif (installExitCode !== 0)\n\t\tthrow new Error(\n\t\t\t`Android app installation failed (exit ${installExitCode}).`\n\t\t);\n\tconst updated = androidInstalledPackageIdentity(\n\t\toptions.project,\n\t\toptions.serial,\n\t\toptions.capture,\n\t\toptions.env\n\t);\n\tawait persistInstalledAndroidNativeCache(options, updated);\n\n\treturn { nativeCacheHit: false, uid: updated?.uid };\n};\n\nconst startManagedEmulatorIfNeeded = (\n\tproject: AbsoluteAndroidDevProject,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tspawn: NonNullable<StartAbsoluteAndroidDevOptions['spawn']>,\n\tlog: NonNullable<StartAbsoluteAndroidDevOptions['log']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tif (!project.emulator) {\n\t\tthrow new Error(\n\t\t\t'Android Emulator is unavailable. Select a physical device or run `absolute mobile doctor android --fix`.'\n\t\t);\n\t}\n\tconst serial = managedEmulatorSerial(project.adb, capture, env);\n\tif (serial) return { serial, started: false };\n\tlog(`Starting Android emulator ${ABSOLUTE_ANDROID_AVD_NAME}...`);\n\tspawn(\n\t\t[\n\t\t\tproject.emulator,\n\t\t\t'-avd',\n\t\t\tABSOLUTE_ANDROID_AVD_NAME,\n\t\t\t'-netdelay',\n\t\t\t'none',\n\t\t\t'-netspeed',\n\t\t\t'full'\n\t\t],\n\t\t{ env }\n\t);\n\n\treturn { serial: undefined, started: true };\n};\n\nconst logHttpsCertificateRequirement = (\n\thttps: boolean | undefined,\n\tlog: NonNullable<StartAbsoluteAndroidDevOptions['log']>\n) => {\n\tif (https !== true) return;\n\tlog(\n\t\t'Android HMR is using HTTPS; AbsoluteJS is projecting the local development CA into this debug app only.'\n\t);\n};\n\nconst removeAdbReverse = async (\n\tproject: AbsoluteAndroidDevProject,\n\tserial: string | undefined,\n\tport: number,\n\trun: NonNullable<StartAbsoluteAndroidDevOptions['run']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tif (!serial) return;\n\tawait run(\n\t\t[project.adb, '-s', serial, 'reverse', '--remove', `tcp:${port}`],\n\t\t{ env }\n\t).catch(() => undefined);\n};\n\nconst androidLaunchCommand = (\n\tproject: AbsoluteAndroidDevProject,\n\tserial: string\n) => [\n\tproject.adb,\n\t'-s',\n\tserial,\n\t'shell',\n\t'monkey',\n\t'-p',\n\tproject.config.appId,\n\t'-c',\n\t'android.intent.category.LAUNCHER',\n\t'1'\n];\n\nconst stopAndroidAppCommand = (\n\tproject: AbsoluteAndroidDevProject,\n\tserial: string\n) => [\n\tproject.adb,\n\t'-s',\n\tserial,\n\t'shell',\n\t'am',\n\t'force-stop',\n\tproject.config.appId\n];\n\nconst launchAndroidApp = async (\n\tproject: AbsoluteAndroidDevProject,\n\tserial: string,\n\tdescription: string,\n\trun: NonNullable<StartAbsoluteAndroidDevOptions['run']>,\n\toptions: {\n\t\tenv: Record<string, string | undefined>;\n\t\tsignal?: AbortSignal;\n\t}\n) => {\n\tawait requireSuccess(\n\t\tstopAndroidAppCommand(project, serial),\n\t\t`${description} reset`,\n\t\trun,\n\t\toptions\n\t);\n\tawait requireSuccess(\n\t\tandroidLaunchCommand(project, serial),\n\t\tdescription,\n\t\trun,\n\t\toptions\n\t);\n};\n\nconst androidPackageUid = (\n\tproject: AbsoluteAndroidDevProject,\n\tserial: string,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tconst result = capture(\n\t\t[\n\t\t\tproject.adb,\n\t\t\t'-s',\n\t\t\tserial,\n\t\t\t'shell',\n\t\t\t'cmd',\n\t\t\t'package',\n\t\t\t'list',\n\t\t\t'packages',\n\t\t\t'-U',\n\t\t\tproject.config.appId\n\t\t],\n\t\t{ env }\n\t);\n\tif (result.exitCode !== 0) return undefined;\n\tconst escapedAppId = project.config.appId.replace(\n\t\t/[.*+?^${}()|[\\]\\\\]/gu,\n\t\t'\\\\$&'\n\t);\n\tconst match = new RegExp(`package:${escapedAppId}\\\\s+uid:(\\\\d+)`, 'u').exec(\n\t\tresult.stdout\n\t);\n\n\treturn match?.[1];\n};\n\nconst androidInstalledPackageIdentity = (\n\tproject: AbsoluteAndroidDevProject,\n\tserial: string,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tenv: Record<string, string | undefined>\n) => {\n\tconst result = capture(\n\t\t[\n\t\t\tproject.adb,\n\t\t\t'-s',\n\t\t\tserial,\n\t\t\t'shell',\n\t\t\t'dumpsys',\n\t\t\t'package',\n\t\t\tproject.config.appId\n\t\t],\n\t\t{ env }\n\t);\n\tif (result.exitCode !== 0) return undefined;\n\tconst codePath = /^\\s*codePath=(.+)$/mu.exec(result.stdout)?.[1]?.trim();\n\tconst lastUpdateTime = /^\\s*lastUpdateTime=(.+)$/mu\n\t\t.exec(result.stdout)?.[1]\n\t\t?.trim();\n\tif (!codePath || !lastUpdateTime) return undefined;\n\tconst uid = /^\\s*appId=(\\d+)$/mu.exec(result.stdout)?.[1];\n\n\treturn { identity: `${codePath}\\0${lastUpdateTime}`, uid };\n};\n\nconst attachAndroidNativeLogs = (\n\tproject: AbsoluteAndroidDevProject,\n\tserial: string,\n\tcapture: NonNullable<StartAbsoluteAndroidDevOptions['capture']>,\n\tenv: Record<string, string | undefined>,\n\toptions: StartAbsoluteAndroidDevOptions,\n\tknownUid?: string\n) => {\n\tif (!options.nativeLog) return null;\n\tconst uid = knownUid ?? androidPackageUid(project, serial, capture, env);\n\tif (!uid) {\n\t\toptions.log?.(\n\t\t\t`Android app logs unavailable: could not resolve the package UID for ${project.config.appId}.`\n\t\t);\n\n\t\treturn null;\n\t}\n\tconst onLine = (line: string) => {\n\t\tconst entry = parseAbsoluteAndroidLogLine(line);\n\t\t// Capacitor 8 SystemBars can run before documentElement exists. A later\n\t\t// inset pass applies the values; keep the plugin enabled because disabling\n\t\t// it breaks edge-to-edge layout on Android 16. Remove this narrow filter\n\t\t// when https://github.com/ionic-team/capacitor/issues/8530 is fixed.\n\t\tif (entry && !isIgnorableAbsoluteAndroidLog(entry)) {\n\t\t\toptions.nativeLog?.(entry);\n\t\t}\n\t};\n\n\treturn (options.startNativeLogs ?? startNativeLogStream)(\n\t\t[\n\t\t\tproject.adb,\n\t\t\t'-s',\n\t\t\tserial,\n\t\t\t'logcat',\n\t\t\t`--uid=${uid}`,\n\t\t\t'-v',\n\t\t\t'threadtime',\n\t\t\t'-T',\n\t\t\t'1',\n\t\t\t'*:W',\n\t\t\t'Capacitor:V',\n\t\t\t'Capacitor/Console:V',\n\t\t\t'chromium:I'\n\t\t],\n\t\t{ env },\n\t\tonLine\n\t);\n};\n\nexport const prepareAbsoluteAndroidDevProject = async (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\toptions: PrepareAbsoluteAndroidDevOptions\n): Promise<AbsoluteAndroidDevProject> => {\n\tconst projectRoot = resolve(options.projectRoot);\n\tconst host = detectAbsoluteMobileHost();\n\tconst androidRoot =\n\t\tprocess.env.ANDROID_HOME ??\n\t\tprocess.env.ANDROID_SDK_ROOT ??\n\t\tabsoluteManagedAndroidSdkRoot(host);\n\tconst checks = await inspectAbsoluteMobileToolchain({ androidRoot, host });\n\tconst deviceOnlyChecks = new Set([\n\t\t'android.avd',\n\t\t'android.avdmanager',\n\t\t'android.emulator',\n\t\t'android.virtualization'\n\t]);\n\tconst failed = checks.filter(\n\t\t(check) =>\n\t\t\tcheck.platform === 'android' &&\n\t\t\t(check.status === 'fail' || check.status === 'warn') &&\n\t\t\t(options.target !== 'device' || !deviceOnlyChecks.has(check.id))\n\t);\n\tif (failed.length > 0) {\n\t\tthrow new Error(\n\t\t\t`Android ${options.target === 'device' ? 'device development' : 'emulation'} is not ready: ${failed.map(({ label }) => label).join(', ')}.`\n\t\t);\n\t}\n\tconst adb = checks.find((check) => check.id === 'android.adb')?.path;\n\tconst emulator = checks.find(\n\t\t(check) => check.id === 'android.emulator'\n\t)?.path;\n\tif (!adb || (options.target !== 'device' && !emulator)) {\n\t\tthrow new Error(\n\t\t\t'Android SDK tools disappeared after readiness checks.'\n\t\t);\n\t}\n\tconst cap = join(\n\t\tprojectRoot,\n\t\t'node_modules',\n\t\t'.bin',\n\t\thost === 'windows' ? 'cap.cmd' : 'cap'\n\t);\n\tif (!(await pathExists(cap))) {\n\t\tthrow new Error(\n\t\t\t'Capacitor CLI is not installed. Run absolute mobile init first.'\n\t\t);\n\t}\n\tawait writeAbsoluteCapacitorConfig(config, { projectRoot });\n\tawait mkdir(config.bundleDirectory, { recursive: true });\n\tconst placeholder = join(config.bundleDirectory, 'index.html');\n\tif (!(await pathExists(placeholder))) {\n\t\tawait writeFile(\n\t\t\tplaceholder,\n\t\t\t'<!doctype html><title>AbsoluteJS mobile development</title>\\n'\n\t\t);\n\t}\n\tconst nativeDirectory = join(config.nativeProjectDirectory, 'android');\n\tif (!(await pathExists(nativeDirectory))) {\n\t\tif (!options.createNativeProject) {\n\t\t\tthrow new Error('Android native project has not been created.');\n\t\t}\n\t\tconst run = options.run ?? runCommand;\n\t\tif (\n\t\t\t(await run([cap, 'add', 'android'], {\n\t\t\t\tcwd: projectRoot\n\t\t\t})) !== 0\n\t\t) {\n\t\t\tthrow new Error('Capacitor Android project creation failed.');\n\t\t}\n\t}\n\n\treturn {\n\t\tadb,\n\t\tandroidRoot,\n\t\tcap,\n\t\tconfig,\n\t\t...(emulator ? { emulator } : {}),\n\t\thost,\n\t\tnativeDirectory,\n\t\tprojectRoot\n\t};\n};\n\nexport const startAbsoluteAndroidDevSession = async (\n\toptions: StartAbsoluteAndroidDevOptions\n) => {\n\tconst { project } = options;\n\tconst startupStartedAt = performance.now();\n\tlet phaseStartedAt = performance.now();\n\tconst phaseDurations: Record<string, number> = {};\n\tlet state: AbsoluteAndroidDevState = 'syncing';\n\tconst transition = (next: AbsoluteAndroidDevState) => {\n\t\tif (next === state) {\n\t\t\toptions.onStateChange?.(next);\n\n\t\t\treturn;\n\t\t}\n\t\tconst now = performance.now();\n\t\tconst durationMs = now - phaseStartedAt;\n\t\tphaseDurations[state] = (phaseDurations[state] ?? 0) + durationMs;\n\t\toptions.onPhaseTiming?.({\n\t\t\tdurationMs,\n\t\t\tphase: state,\n\t\t\ttotalMs: now - startupStartedAt\n\t\t});\n\t\tstate = next;\n\t\tphaseStartedAt = now;\n\t\toptions.onStateChange?.(next);\n\t};\n\tconst capture = options.capture ?? captureCommand;\n\tconst run = options.run ?? runCommand;\n\tconst sleep = options.sleep ?? Bun.sleep;\n\tconst spawn = options.spawn ?? spawnCommand;\n\tconst log = options.log ?? console.log;\n\tconst androidHome =\n\t\tproject.host === 'wsl'\n\t\t\t? windowsPathFromWsl(project.androidRoot, capture)\n\t\t\t: project.androidRoot;\n\tconst env: Record<string, string | undefined> = {\n\t\t...process.env,\n\t\tANDROID_HOME: androidHome\n\t};\n\ttransition('syncing');\n\tawait repairAbsoluteAndroidDevSession(project.projectRoot);\n\tthrowIfAborted(options.signal);\n\tawait requireSuccess(\n\t\t[project.cap, 'sync', 'android'],\n\t\t'Capacitor Android synchronization',\n\t\trun,\n\t\t{ cwd: project.projectRoot, env, signal: options.signal }\n\t);\n\tthrowIfAborted(options.signal);\n\ttransition('configuring');\n\tconst nativeConfigPath = join(\n\t\tproject.nativeDirectory,\n\t\t'app',\n\t\t'src',\n\t\t'main',\n\t\t'assets',\n\t\t'capacitor.config.json'\n\t);\n\tconst nativeManifestPath = join(\n\t\tproject.nativeDirectory,\n\t\t'app',\n\t\t'src',\n\t\t'main',\n\t\t'AndroidManifest.xml'\n\t);\n\tlet connectedSerial: string | undefined;\n\tlet nativeLogs: AbsoluteAndroidLogStream | null = null;\n\tconst closeNativeLogs = async () => {\n\t\tconst stream = nativeLogs;\n\t\tnativeLogs = null;\n\t\tawait stream?.close().catch(() => undefined);\n\t};\n\ttry {\n\t\tconst physicalDevice = options.deviceSerial !== undefined;\n\t\tif (options.https && !options.certificateAuthorityPath) {\n\t\t\tthrow new Error(\n\t\t\t\t'Android HTTPS development requires the AbsoluteJS development CA certificate.'\n\t\t\t);\n\t\t}\n\t\tawait writeDevConfig(\n\t\t\tproject.projectRoot,\n\t\t\tnativeConfigPath,\n\t\t\tnativeManifestPath,\n\t\t\toptions.port,\n\t\t\toptions.https === true,\n\t\t\tproject.config.entry,\n\t\t\toptions.embeddedBundle === true,\n\t\t\toptions.serverHost ?? 'localhost',\n\t\t\toptions.certificateAuthorityPath\n\t\t);\n\t\tthrowIfAborted(options.signal);\n\t\tlogHttpsCertificateRequirement(options.https, log);\n\t\tconst fingerprintStartedAt = performance.now();\n\t\tconst nativeFingerprintPromise =\n\t\t\tfingerprintAbsoluteAndroidNativeProject(project, {\n\t\t\t\tincludePublicBundle: options.embeddedBundle === true\n\t\t\t}).then((fingerprint) => {\n\t\t\t\tphaseDurations.fingerprinting =\n\t\t\t\t\tperformance.now() - fingerprintStartedAt;\n\n\t\t\t\treturn fingerprint;\n\t\t\t});\n\t\ttransition('booting');\n\t\tif (options.deviceSerial)\n\t\t\trequireConnectedAndroidDevice(\n\t\t\t\tproject.adb,\n\t\t\t\toptions.deviceSerial,\n\t\t\t\tcapture,\n\t\t\t\tenv\n\t\t\t);\n\t\tconst emulator = options.deviceSerial\n\t\t\t? { serial: options.deviceSerial, started: false }\n\t\t\t: startManagedEmulatorIfNeeded(project, capture, spawn, log, env);\n\t\tconst { started: startedEmulator } = emulator;\n\t\ttransition('connecting');\n\t\tconst serial = await waitForManagedEmulator(\n\t\t\tproject,\n\t\t\tcapture,\n\t\t\tsleep,\n\t\t\tenv,\n\t\t\toptions.signal,\n\t\t\temulator.serial\n\t\t);\n\t\tconnectedSerial = serial;\n\t\ttransition('forwarding');\n\t\tif (!physicalDevice)\n\t\t\tawait requireSuccess(\n\t\t\t\t[\n\t\t\t\t\tproject.adb,\n\t\t\t\t\t'-s',\n\t\t\t\t\tserial,\n\t\t\t\t\t'reverse',\n\t\t\t\t\t`tcp:${options.port}`,\n\t\t\t\t\t`tcp:${options.port}`\n\t\t\t\t],\n\t\t\t\t'ADB reverse port forwarding',\n\t\t\t\trun,\n\t\t\t\t{ env, signal: options.signal }\n\t\t\t);\n\t\tthrowIfAborted(options.signal);\n\t\tconst nativeFingerprint = await nativeFingerprintPromise;\n\t\tthrowIfAborted(options.signal);\n\t\ttransition('checking-native');\n\t\tconst cache = await readNativeCache(project.projectRoot);\n\t\tconst installedApp = await ensureAndroidDebugApp({\n\t\t\tcache,\n\t\t\tcapture,\n\t\t\tenv,\n\t\t\tfingerprint: nativeFingerprint,\n\t\t\tlog,\n\t\t\tproject,\n\t\t\trun,\n\t\t\tserial,\n\t\t\tsignal: options.signal,\n\t\t\ttransition\n\t\t});\n\t\tconst { nativeCacheHit, uid } = installedApp;\n\t\tthrowIfAborted(options.signal);\n\t\ttransition('launching');\n\t\tawait launchAndroidApp(project, serial, 'Android app launch', run, {\n\t\t\tenv,\n\t\t\tsignal: options.signal\n\t\t});\n\t\tthrowIfAborted(options.signal);\n\t\tif (options.nativeLog) transition('streaming-logs');\n\t\tnativeLogs = attachAndroidNativeLogs(\n\t\t\tproject,\n\t\t\tserial,\n\t\t\tcapture,\n\t\t\tenv,\n\t\t\toptions,\n\t\t\tuid\n\t\t);\n\t\ttransition('ready');\n\t\tphaseDurations.total = performance.now() - startupStartedAt;\n\t\tlog(\n\t\t\toptions.embeddedBundle === true\n\t\t\t\t? `Android ${physicalDevice ? 'device' : 'emulator'} connected (${serial}) with the embedded bundle and backend on port ${options.port} in ${getDurationString(phaseDurations.total)} (${nativeCacheHit ? 'native cache hit' : 'native build installed'}).`\n\t\t\t\t: `Android ${physicalDevice ? 'device' : 'emulator'} connected (${serial}) with HMR on port ${options.port} in ${getDurationString(phaseDurations.total)} (${nativeCacheHit ? 'native cache hit' : 'native build installed'}).`\n\t\t);\n\t\tlog(\n\t\t\t`Android startup: ${androidTimingSummary(phaseDurations, physicalDevice)}.`\n\t\t);\n\t\tlet closed = false;\n\t\tconst close = async () => {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\ttransition('closing');\n\t\t\tawait closeNativeLogs();\n\t\t\tif (!physicalDevice)\n\t\t\t\tawait removeAdbReverse(project, serial, options.port, run, env);\n\t\t\tawait repairAbsoluteAndroidDevSession(project.projectRoot);\n\t\t\ttransition('closed');\n\t\t};\n\n\t\treturn {\n\t\t\tclose,\n\t\t\tnativeCacheHit,\n\t\t\tserial,\n\t\t\tstartedEmulator,\n\t\t\ttimings: { ...phaseDurations },\n\t\t\trebuild: async () => {\n\t\t\t\tif (closed) {\n\t\t\t\t\tthrow new Error('Android development session is closed.');\n\t\t\t\t}\n\t\t\t\tlog(\n\t\t\t\t\t'Android native inputs changed; rebuilding without restarting the dev server.'\n\t\t\t\t);\n\t\t\t\tawait close();\n\n\t\t\t\treturn startAbsoluteAndroidDevSession(options);\n\t\t\t},\n\t\t\trelaunch: async () => {\n\t\t\t\tif (closed) {\n\t\t\t\t\tthrow new Error('Android development session is closed.');\n\t\t\t\t}\n\t\t\t\ttransition('launching');\n\t\t\t\ttry {\n\t\t\t\t\tawait launchAndroidApp(\n\t\t\t\t\t\tproject,\n\t\t\t\t\t\tserial,\n\t\t\t\t\t\t'Android app relaunch',\n\t\t\t\t\t\trun,\n\t\t\t\t\t\t{ env, signal: options.signal }\n\t\t\t\t\t);\n\t\t\t\t\ttransition('ready');\n\t\t\t\t\tlog(`Android app relaunched on ${serial}.`);\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttransition('failed');\n\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t},\n\t\t\tget state() {\n\t\t\t\treturn state;\n\t\t\t}\n\t\t};\n\t} catch (error) {\n\t\ttransition('failed');\n\t\tawait closeNativeLogs();\n\t\tif (!options.deviceSerial)\n\t\t\tawait removeAdbReverse(\n\t\t\t\tproject,\n\t\t\t\tconnectedSerial,\n\t\t\t\toptions.port,\n\t\t\t\trun,\n\t\t\t\tenv\n\t\t\t);\n\t\tawait repairAbsoluteAndroidDevSession(project.projectRoot);\n\n\t\tthrow error;\n\t}\n};\n",
|
|
38
38
|
"import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { relative, resolve } from 'node:path';\nimport type { NormalizedAbsoluteMobileConfig } from './config';\n\nexport type WriteAbsoluteCapacitorConfigOptions = {\n\tforce?: boolean;\n\tprojectRoot: string;\n};\n\nconst CONFIG_FILE = 'capacitor.config.ts';\n\nconst portableRelative = (root: string, path: string) =>\n\trelative(root, path).replaceAll('\\\\', '/');\n\nconst capacitorConfigSource = (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\tprojectRoot: string\n) => `import type { CapacitorConfig } from '@capacitor/cli';\n\nconst config: CapacitorConfig = {\n\tappId: ${JSON.stringify(config.appId)},\n\tappName: ${JSON.stringify(config.appName)},\n\twebDir: ${JSON.stringify(portableRelative(projectRoot, config.bundleDirectory))},\n\tandroid: {\n\t\tpath: ${JSON.stringify(portableRelative(projectRoot, `${config.nativeProjectDirectory}/android`))}\n\t},\n\tios: {\n\t\tpath: ${JSON.stringify(portableRelative(projectRoot, `${config.nativeProjectDirectory}/ios`))}\n\t}\n};\n\nexport default config;\n`;\n\nconst exists = async (path: string) => {\n\ttry {\n\t\tawait access(path);\n\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\n// Capacitor needs an index to finish adding a platform, even before the first\n// production bundle. This scaffold deliberately creates no bundle manifest.\nexport const prepareAbsoluteCapacitorInitialization = async (\n\tconfig: NormalizedAbsoluteMobileConfig\n) => {\n\tawait mkdir(config.bundleDirectory, { recursive: true });\n\ttry {\n\t\tawait writeFile(\n\t\t\tresolve(config.bundleDirectory, 'index.html'),\n\t\t\t'<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>App preparation</title><body><main><h1>Your native project is ready</h1><p>Build your AbsoluteJS mobile bundle before running this app.</p></main></body></html>',\n\t\t\t{ flag: 'wx' }\n\t\t);\n\t} catch (error) {\n\t\tif (!(\n\t\t\terror instanceof Error &&\n\t\t\t'code' in error &&\n\t\t\terror.code === 'EEXIST'\n\t\t))\n\t\t\tthrow error;\n\t}\n\n\treturn Promise.all(\n\t\tconfig.platforms.map(async (platform) => ({\n\t\t\tcommand: (await exists(\n\t\t\t\tresolve(config.nativeProjectDirectory, platform)\n\t\t\t))\n\t\t\t\t? 'sync'\n\t\t\t\t: 'add',\n\t\t\tplatform\n\t\t}))\n\t);\n};\n\nexport const writeAbsoluteCapacitorConfig = async (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\toptions: WriteAbsoluteCapacitorConfigOptions\n) => {\n\tconst projectRoot = resolve(options.projectRoot);\n\tconst destination = resolve(projectRoot, CONFIG_FILE);\n\tconst source = capacitorConfigSource(config, projectRoot);\n\tif (await exists(destination)) {\n\t\tconst current = await readFile(destination, 'utf8');\n\t\tif (current === source) return { changed: false, path: destination };\n\t\tif (!options.force) {\n\t\t\tthrow new TypeError(\n\t\t\t\t`${CONFIG_FILE} already exists and differs; rerun with --force only after reviewing it.`\n\t\t\t);\n\t\t}\n\t}\n\tconst temporary = `${destination}.${crypto.randomUUID()}.tmp`;\n\tawait writeFile(temporary, source, { flag: 'wx' });\n\tawait rename(temporary, destination);\n\n\treturn { changed: true, path: destination };\n};\n",
|
|
39
39
|
"import { spawn, type ChildProcess } from 'node:child_process';\nimport { access } from 'node:fs/promises';\nimport { createServer } from 'node:net';\nimport { join, resolve as resolvePath } from 'node:path';\nimport type { NormalizedAbsoluteMobileConfig } from './config';\nimport {\n\tabsoluteManagedAndroidSdkRoot,\n\tdetectAbsoluteMobileHost,\n\ttype AbsoluteMobileHost\n} from './emulatorDoctor';\nimport {\n\tstartAbsoluteIosCaEnrollmentServer,\n\ttype AbsoluteIosCaEnrollmentServer\n} from './iosPhysicalDeviceTransport';\n\nexport type AbsoluteExpoDevPlatform = 'android' | 'ios';\nexport type AbsoluteExpoDevState =\n\t| 'building-android'\n\t| 'building-ios'\n\t| 'closed'\n\t| 'enrolling-trust'\n\t| 'failed'\n\t| 'preparing-native'\n\t| 'ready'\n\t| 'starting-metro';\n\nexport type AbsoluteExpoDevCommand = {\n\targs: string[];\n\tenv: Record<string, string>;\n\tplatform?: AbsoluteExpoDevPlatform;\n\trole: 'metro' | 'native-build' | 'native-prepare';\n};\n\nexport type AbsoluteExpoDevPlan = {\n\tcommands: AbsoluteExpoDevCommand[];\n\tmetroPort: number;\n\tproject: string;\n};\n\nexport type PlanAbsoluteExpoDevOptions = {\n\tandroidDevice?: string;\n\tandroidOrigin?: string;\n\tcertificateAuthorityPath?: string;\n\tiosDevice?: string;\n\tiosOrigin?: string;\n\tmetroHost?: string;\n\tmetroPort: number;\n\t/** The caller owns Metro, for example through a Remote Mac tunnel. */\n\tmetro?: 'external' | 'managed';\n\tplatforms: AbsoluteExpoDevPlatform[];\n};\n\nexport type AbsoluteExpoDevPhaseTiming = {\n\tdurationMs: number;\n\tphase: AbsoluteExpoDevState;\n\treason?: 'metro-port-conflict';\n\tretryCount?: number;\n};\n\nexport type StartAbsoluteExpoDevOptions = PlanAbsoluteExpoDevOptions & {\n\tandroidAdb?: string;\n\tandroidRoot?: string;\n\tcapture?: (command: string[]) => { exitCode: number; stdout: string };\n\tconfig: NormalizedAbsoluteMobileConfig;\n\texecutable?: string;\n\thost?: AbsoluteMobileHost;\n\tallocateMetroPort?: (excludedPorts: readonly number[]) => Promise<number>;\n\tlog?: (message: string) => void;\n\tonPhaseTiming?: (timing: AbsoluteExpoDevPhaseTiming) => void;\n\tonStateChange?: (state: AbsoluteExpoDevState) => void;\n\tsignal?: AbortSignal;\n\tspawnProcess?: typeof spawn;\n\tstartCaEnrollmentServer?: typeof startAbsoluteIosCaEnrollmentServer;\n};\n\nexport type AbsoluteExpoDevSession = {\n\tclose: () => Promise<void>;\n\tmetroPort: number;\n\tplatforms: AbsoluteExpoDevPlatform[];\n\trebuild: () => Promise<AbsoluteExpoDevSession>;\n\ttimings: Partial<Record<AbsoluteExpoDevState, number>>;\n};\n\nexport type InstallAbsoluteExpoAndroidReleaseOptions = {\n\tandroidAdb?: string;\n\tandroidDevice?: string;\n\tandroidRoot?: string;\n\tcapture?: (command: string[]) => { exitCode: number; stdout: string };\n\tconfig: NormalizedAbsoluteMobileConfig;\n\texecutable?: string;\n\tforwardedPort: number;\n\thost?: AbsoluteMobileHost;\n\tlog?: (message: string) => void;\n\tsignal?: AbortSignal;\n\tspawnProcess?: typeof spawn;\n};\n\nexport type AbsoluteExpoAndroidReleaseInstallation = {\n\tdurationMs: number;\n\tserial: string;\n};\n\ntype AbsoluteExpoWindowsBuildCommandOptions = {\n\tenv?: Record<string, string | undefined>;\n\tsignal?: AbortSignal;\n};\n\nexport type BuildAbsoluteExpoAndroidWslBundleOptions = {\n\tandroidRoot: string;\n\tcapture?: (command: string[]) => { exitCode: number; stdout: string };\n\tenv?: Record<string, string | undefined>;\n\tgradleArguments?: readonly string[];\n\tproject: string;\n\trun?: (\n\t\tcommand: string[],\n\t\toptions?: AbsoluteExpoWindowsBuildCommandOptions\n\t) => Promise<number>;\n\tsignal?: AbortSignal;\n};\n\nexport type InstallAbsoluteExpoIosReleaseOptions = {\n\tcapture?: (command: string[]) => { exitCode: number; stdout: string };\n\tconfig: NormalizedAbsoluteMobileConfig;\n\texecutable?: string;\n\tiosDevice?: string;\n\tlog?: (message: string) => void;\n\tsignal?: AbortSignal;\n\tspawnProcess?: typeof spawn;\n\txcrun?: string;\n};\n\nexport type AbsoluteExpoIosReleaseInstallation = {\n\tdurationMs: number;\n\tudid: string;\n};\n\nconst METRO_READY_TIMEOUT_MS = 120_000;\nconst METRO_START_MAX_ATTEMPTS = 4;\nconst MAX_TCP_PORT = 65_535;\nconst PROCESS_CLOSE_TIMEOUT_MS = 2_000;\nconst HASH_RADIX = 16;\nconst WINDOWS_BUILD_ID_LENGTH = 10;\nconst DEFAULT_WINDOWS_BUILD_RETENTION_DAYS = 14;\nconst WINDOWS_BUILD_STAMP = '.absolutejs-last-build';\n\n/** Managed Windows mirrors are keyed by project path and each one carries a\n * full node_modules plus Gradle output, so mirrors that have not built within\n * the retention window are removed. Override with\n * ABSOLUTE_EXPO_BUILD_RETENTION_DAYS. */\nconst windowsBuildRetentionDays = () => {\n\tconst configured = Number(process.env.ABSOLUTE_EXPO_BUILD_RETENTION_DAYS);\n\n\treturn Number.isFinite(configured) && configured > 0\n\t\t? configured\n\t\t: DEFAULT_WINDOWS_BUILD_RETENTION_DAYS;\n};\n\nconst allocateAvailableMetroPort = () =>\n\tnew Promise<number>((resolve, reject) => {\n\t\tconst server = createServer();\n\t\tserver.unref();\n\t\tserver.once('error', reject);\n\t\tserver.listen(0, '127.0.0.1', () => {\n\t\t\tconst address = server.address();\n\t\t\tif (!address || typeof address === 'string') {\n\t\t\t\tserver.close();\n\t\t\t\treject(\n\t\t\t\t\tnew Error('Failed to allocate a replacement Metro port.')\n\t\t\t\t);\n\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tserver.close((error) => {\n\t\t\t\tif (error) reject(error);\n\t\t\t\telse resolve(address.port);\n\t\t\t});\n\t\t});\n\t});\n\nconst metroPortConflict = (line: string) =>\n\t/(?:EADDRINUSE|address already in use|port \\d+ is being used by another process)/iu.test(\n\t\tline\n\t);\n\nclass MetroStartupError extends Error {\n\tconflict: boolean;\n\n\tconstructor(exitCode: number, conflict: boolean) {\n\t\tsuper(`Expo Metro exited with status ${exitCode}.`);\n\t\tthis.conflict = conflict;\n\t}\n}\n\nconst preserveWindowsSubstRealpaths = `\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst physicalRoot = process.env.ABSOLUTE_EXPO_PHYSICAL_ROOT;\nconst mappedRoot = process.env.ABSOLUTE_EXPO_MAPPED_ROOT;\nconst remap = (value) => {\n\tif (typeof value !== 'string' || !physicalRoot || !mappedRoot) return value;\n\tconst normalizedValue = path.resolve(value);\n\tconst normalizedRoot = path.resolve(physicalRoot);\n\tif (normalizedValue.toLowerCase() === normalizedRoot.toLowerCase()) return mappedRoot;\n\tconst prefix = normalizedRoot.endsWith(path.sep) ? normalizedRoot : normalizedRoot + path.sep;\n\treturn normalizedValue.toLowerCase().startsWith(prefix.toLowerCase())\n\t\t? path.join(mappedRoot, normalizedValue.slice(prefix.length))\n\t\t: value;\n};\nconst realpath = fs.realpath.bind(fs);\nconst realpathSync = fs.realpathSync.bind(fs);\nconst realpathNative = fs.realpathSync.native?.bind(fs.realpathSync);\nfs.realpath = (value, options, callback) => {\n\tconst done = typeof options === 'function' ? options : callback;\n\tconst encoding = typeof options === 'function' ? undefined : options;\n\treturn realpath(value, encoding, (error, result) => done(error, remap(result)));\n};\nfs.realpathSync = (value, options) => remap(realpathSync(value, options));\nif (realpathNative) fs.realpathSync.native = (value, options) => remap(realpathNative(value, options));\nconst promiseRealpath = fs.promises.realpath.bind(fs.promises);\nfs.promises.realpath = async (value, options) => remap(await promiseRealpath(value, options));\n`;\n\nconst expoDevelopmentClientScheme = (appId: string) =>\n\t`exp+${appId.toLowerCase().replaceAll(/[^a-z0-9]+/gu, '-')}`;\n\nconst expoDevelopmentClientUrl = (appId: string, host: string, port: number) =>\n\t`${expoDevelopmentClientScheme(appId)}://expo-development-client/?url=${encodeURIComponent(`http://${host}:${port}`)}`;\n\nconst captureCommand = (command: string[]) => {\n\tconst result = Bun.spawnSync(command, {\n\t\tkillSignal: 'SIGKILL',\n\t\tstderr: 'ignore',\n\t\tstdout: 'pipe',\n\t\ttimeout: 30_000\n\t});\n\n\treturn { exitCode: result.exitCode, stdout: result.stdout.toString() };\n};\n\nconst windowsPathFromWsl = (\n\tpath: string,\n\tcapture: NonNullable<StartAbsoluteExpoDevOptions['capture']>\n) => {\n\tconst result = capture(['wslpath', '-w', path]);\n\tif (result.exitCode !== 0 || !result.stdout.trim()) {\n\t\tthrow new Error(`Could not translate WSL path for Windows: ${path}`);\n\t}\n\n\treturn result.stdout.trim();\n};\n\nconst absoluteExpoWindowsBuildDirectory = (\n\tproject: string,\n\tandroidRoot: string\n) => {\n\tconst buildId = Bun.hash(resolvePath(project)).toString(HASH_RADIX);\n\n\treturn resolvePath(\n\t\tandroidRoot,\n\t\t'..',\n\t\t'..',\n\t\t'ExpoBuilds',\n\t\tbuildId.slice(0, WINDOWS_BUILD_ID_LENGTH)\n\t);\n};\n\nconst connectLocalExpoAndroid = (\n\tadb: string,\n\tappId: string,\n\tmetroPort: number,\n\tcapture: NonNullable<StartAbsoluteExpoDevOptions['capture']>,\n\tpreferredDevice?: string\n) => {\n\tconst devices = capture([adb, 'devices']);\n\tif (devices.exitCode !== 0)\n\t\tthrow new Error(\n\t\t\t'Could not inspect devices after the Expo Android build.'\n\t\t);\n\tconst ready = devices.stdout.split(/\\r?\\n/u).flatMap((line) => {\n\t\tconst match = /^(\\S+)\\s+device$/u.exec(line.trim());\n\n\t\treturn match?.[1] ? [match[1]] : [];\n\t});\n\tconst serial = preferredDevice\n\t\t? ready.find((value) => value === preferredDevice)\n\t\t: (ready.find((value) => value.startsWith('emulator-')) ?? ready[0]);\n\tif (!serial)\n\t\tthrow new Error(\n\t\t\tpreferredDevice\n\t\t\t\t? 'The selected Expo Android device is not connected after build.'\n\t\t\t\t: 'Expo Android build completed but no ready device was found.'\n\t\t);\n\tconst run = (args: string[], message: string) => {\n\t\tif (capture([adb, '-s', serial, ...args]).exitCode !== 0)\n\t\t\tthrow new Error(message);\n\t};\n\trun(\n\t\t['reverse', `tcp:${metroPort}`, `tcp:${metroPort}`],\n\t\t'Expo Android Metro forwarding failed.'\n\t);\n\trun(\n\t\t['shell', 'am', 'force-stop', appId],\n\t\t'Expo Android development-client reset failed.'\n\t);\n\tconst openDevelopmentClient = () =>\n\t\trun(\n\t\t\t[\n\t\t\t\t'shell',\n\t\t\t\t'am',\n\t\t\t\t'start',\n\t\t\t\t'-a',\n\t\t\t\t'android.intent.action.VIEW',\n\t\t\t\t'-d',\n\t\t\t\texpoDevelopmentClientUrl(appId, 'localhost', metroPort),\n\t\t\t\tappId\n\t\t\t],\n\t\t\t'Expo Android development-client launch failed.'\n\t\t);\n\topenDevelopmentClient();\n\trun(\n\t\t['shell', 'sleep', '1'],\n\t\t'Expo Android development-client readiness pause failed.'\n\t);\n\topenDevelopmentClient();\n\n\treturn serial;\n};\n\nconst launchLocalExpoAndroidRelease = (\n\tadb: string,\n\tappId: string,\n\tforwardedPort: number,\n\tcapture: NonNullable<StartAbsoluteExpoDevOptions['capture']>,\n\tpreferredDevice?: string\n) => {\n\tconst devices = capture([adb, 'devices']);\n\tif (devices.exitCode !== 0)\n\t\tthrow new Error(\n\t\t\t'Could not inspect devices after the Expo release build.'\n\t\t);\n\tconst ready = devices.stdout.split(/\\r?\\n/u).flatMap((line) => {\n\t\tconst match = /^(\\S+)\\s+device$/u.exec(line.trim());\n\n\t\treturn match?.[1] ? [match[1]] : [];\n\t});\n\tconst serial = preferredDevice\n\t\t? ready.find((value) => value === preferredDevice)\n\t\t: (ready.find((value) => value.startsWith('emulator-')) ?? ready[0]);\n\tif (!serial)\n\t\tthrow new Error(\n\t\t\t'Expo release build completed but no ready device was found.'\n\t\t);\n\tconst run = (args: string[], message: string) => {\n\t\tif (capture([adb, '-s', serial, ...args]).exitCode !== 0)\n\t\t\tthrow new Error(message);\n\t};\n\trun(\n\t\t['reverse', `tcp:${forwardedPort}`, `tcp:${forwardedPort}`],\n\t\t'Expo Android release forwarding failed.'\n\t);\n\trun(\n\t\t['shell', 'am', 'force-stop', appId],\n\t\t'Expo Android release reset failed.'\n\t);\n\trun(\n\t\t[\n\t\t\t'shell',\n\t\t\t'monkey',\n\t\t\t'-p',\n\t\t\tappId,\n\t\t\t'-c',\n\t\t\t'android.intent.category.LAUNCHER',\n\t\t\t'1'\n\t\t],\n\t\t'Expo Android release launch failed.'\n\t);\n\n\treturn serial;\n};\n\nconst encodedWindowsExpoAndroidCommand = (\n\tproject: string,\n\tandroidRoot: string,\n\tappId: string,\n\targs: readonly string[],\n\tcapture: NonNullable<StartAbsoluteExpoDevOptions['capture']>,\n\tforwardedPort: number,\n\tpreferredDevice?: string,\n\tmode: 'bundle' | 'development' | 'release' = 'development',\n\tgradleArguments: readonly string[] = []\n) => {\n\tconst windowsSource = windowsPathFromWsl(project, capture);\n\tconst buildDirectory = absoluteExpoWindowsBuildDirectory(\n\t\tproject,\n\t\tandroidRoot\n\t);\n\tconst windowsDirectory = windowsPathFromWsl(buildDirectory, capture);\n\tconst windowsAndroidRoot = windowsPathFromWsl(androidRoot, capture);\n\tconst encode = (value: string) =>\n\t\tBuffer.from(value, 'utf8').toString('base64');\n\tconst developmentScheme = expoDevelopmentClientScheme(appId);\n\tconst realpathHook = encode(preserveWindowsSubstRealpaths);\n\tconst mirrorMutex = `Local\\\\AbsoluteJS.Expo.${Bun.hash(buildDirectory).toString(HASH_RADIX)}`;\n\tconst deviceCommands =\n\t\tmode === 'bundle'\n\t\t\t? []\n\t\t\t: [\n\t\t\t\t\t\"$adb = Join-Path $androidHome 'platform-tools\\\\adb.exe'\",\n\t\t\t\t\t'$deviceDeadline = [DateTime]::UtcNow.AddSeconds(60)',\n\t\t\t\t\t\"do { $readyDevices = @(& $adb devices) | Where-Object { $_ -match '\\\\tdevice$' } | ForEach-Object { ($_ -split '\\\\s+')[0] }; if (-not $readyDevices) { Start-Sleep -Milliseconds 500 } } while (-not $readyDevices -and [DateTime]::UtcNow -lt $deviceDeadline)\",\n\t\t\t\t\t...(preferredDevice\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`$serial = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encode(preferredDevice)}'))`,\n\t\t\t\t\t\t\t\t\"if ($readyDevices -notcontains $serial) { throw 'The selected Expo Android device is not connected after build.' }\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: [\n\t\t\t\t\t\t\t\t\"$serial = $readyDevices | Where-Object { $_ -like 'emulator-*' } | Select-Object -First 1\",\n\t\t\t\t\t\t\t\t'if (-not $serial) { $serial = $readyDevices | Select-Object -First 1 }'\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\"if (-not $serial) { throw 'Expo Android build completed but no ready device was found.' }\",\n\t\t\t\t\t...(mode === 'release'\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\"$apk = Join-Path $mappedProject 'android\\\\app\\\\build\\\\outputs\\\\apk\\\\release\\\\app-release.apk'\",\n\t\t\t\t\t\t\t\t\"$installOutput = Join-Path $env:TEMP ('absolutejs-adb-install-' + [Guid]::NewGuid().ToString('N') + '.out')\",\n\t\t\t\t\t\t\t\t\"$installError = $installOutput + '.err'\",\n\t\t\t\t\t\t\t\t\"$install = Start-Process -FilePath $adb -ArgumentList @('-s', $serial, 'install', '-r', $apk) -NoNewWindow -PassThru -RedirectStandardOutput $installOutput -RedirectStandardError $installError\",\n\t\t\t\t\t\t\t\t\"if (-not $install.WaitForExit(900000)) { $install.Kill(); throw 'Expo Android release installation timed out after 15 minutes.' }\",\n\t\t\t\t\t\t\t\t'$install.WaitForExit()',\n\t\t\t\t\t\t\t\t'$installText = ((Get-Content $installOutput -Raw -ErrorAction SilentlyContinue) + (Get-Content $installError -Raw -ErrorAction SilentlyContinue))',\n\t\t\t\t\t\t\t\t'if ($installText) { Write-Output $installText.Trim() }',\n\t\t\t\t\t\t\t\t'Remove-Item $installOutput, $installError -Force -ErrorAction SilentlyContinue',\n\t\t\t\t\t\t\t\t\"if ($install.ExitCode -ne 0 -and $installText -notmatch '(?m)^Success\\\\r?$') { throw 'Expo Android release installation failed.' }\",\n\t\t\t\t\t\t\t\t'$installedPackage = @(& $adb -s $serial shell pm path ' +\n\t\t\t\t\t\t\t\t\t`'${appId}')`,\n\t\t\t\t\t\t\t\t\"if ($LASTEXITCODE -ne 0 -or $installedPackage -notmatch '^package:') { throw 'Expo Android release package verification failed.' }\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t\t`& $adb -s $serial reverse 'tcp:${forwardedPort}' 'tcp:${forwardedPort}'`,\n\t\t\t\t\t\"if ($LASTEXITCODE -ne 0) { throw 'Expo Android port forwarding failed.' }\",\n\t\t\t\t\t`& $adb -s $serial shell am force-stop '${appId}'`,\n\t\t\t\t\t\"if ($LASTEXITCODE -ne 0) { throw 'Expo Android application reset failed.' }\",\n\t\t\t\t\t...(mode === 'development'\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t`$developmentUrl = '${developmentScheme}://expo-development-client/?url=' + [Uri]::EscapeDataString('http://localhost:${forwardedPort}')`,\n\t\t\t\t\t\t\t\t`& $adb -s $serial shell am start -a android.intent.action.VIEW -d $developmentUrl '${appId}'`,\n\t\t\t\t\t\t\t\t\"if ($LASTEXITCODE -ne 0) { throw 'Expo Android development-client launch failed.' }\",\n\t\t\t\t\t\t\t\t'Start-Sleep -Milliseconds 1000',\n\t\t\t\t\t\t\t\t`& $adb -s $serial shell am start -a android.intent.action.VIEW -d $developmentUrl '${appId}'`,\n\t\t\t\t\t\t\t\t\"if ($LASTEXITCODE -ne 0) { throw 'Expo Android development-client retry failed.' }\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: [\n\t\t\t\t\t\t\t\t`& $adb -s $serial shell monkey -p '${appId}' -c android.intent.category.LAUNCHER 1 | Out-Null`,\n\t\t\t\t\t\t\t\t\"if ($LASTEXITCODE -ne 0) { throw 'Expo Android release launch failed.' }\"\n\t\t\t\t\t\t\t])\n\t\t\t\t];\n\tconst mappedBuild = [\n\t\t\"$mappedProject = Join-Path ($drive + '\\\\') (Split-Path -Leaf $directory)\",\n\t\t'Set-Location $mappedProject',\n\t\t\"$hook = Join-Path $mappedProject '.absolutejs-preserve-subst.cjs'\",\n\t\t`[IO.File]::WriteAllText($hook, [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${realpathHook}')))`,\n\t\t'$env:ABSOLUTE_EXPO_PHYSICAL_ROOT = $directory',\n\t\t'$env:ABSOLUTE_EXPO_MAPPED_ROOT = $mappedProject',\n\t\t'$env:ABSOLUTE_EXPO_APP_ROOT = $mappedProject',\n\t\t'$env:NODE_OPTIONS = \"--require=$hook\"',\n\t\t// A persistent daemon launched through WSL can inherit the interop pipe\n\t\t// and keep an otherwise completed Expo build open indefinitely.\n\t\t\"$env:GRADLE_OPTS = (($env:GRADLE_OPTS + ' -Dorg.gradle.daemon=false').Trim())\",\n\t\t\"$autolinkingCache = Join-Path $mappedProject 'android\\\\build\\\\generated\\\\autolinking'\",\n\t\t'if ([IO.Directory]::Exists($autolinkingCache)) { [IO.Directory]::Delete($autolinkingCache, $true) }',\n\t\t\"$reactGeneratedResources = Join-Path $mappedProject 'android\\\\app\\\\build\\\\generated\\\\res\\\\react'\",\n\t\t'if ([IO.Directory]::Exists($reactGeneratedResources)) { [IO.Directory]::Delete($reactGeneratedResources, $true) }',\n\t\t...(mode === 'development'\n\t\t\t? [\n\t\t\t\t\t\"$expo = Join-Path $mappedProject 'node_modules\\\\.bin\\\\expo.exe'\",\n\t\t\t\t\t'& $expo @expoArguments',\n\t\t\t\t\t\"if ($LASTEXITCODE -ne 0) { throw 'Expo Android native build failed.' }\"\n\t\t\t\t]\n\t\t\t: [\n\t\t\t\t\t\"$gradle = Join-Path $mappedProject 'android\\\\gradlew.bat'\",\n\t\t\t\t\t\"$androidProject = Join-Path $mappedProject 'android'\",\n\t\t\t\t\tmode === 'bundle'\n\t\t\t\t\t\t? '& $gradle --no-daemon --console=plain -p $androidProject @gradleArguments bundleRelease'\n\t\t\t\t\t\t: '& $gradle --no-daemon --console=plain -p $androidProject assembleRelease',\n\t\t\t\t\t\"if ($LASTEXITCODE -ne 0) { throw 'Expo Android release compilation failed.' }\"\n\t\t\t\t]),\n\t\t...deviceCommands\n\t].join('; ');\n\tconst source = [\n\t\t\"$ErrorActionPreference = 'Stop'\",\n\t\t\"$ProgressPreference = 'SilentlyContinue'\",\n\t\t`$source = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encode(windowsSource)}'))`,\n\t\t`$directory = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encode(windowsDirectory)}'))`,\n\t\t`$androidHome = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encode(windowsAndroidRoot)}'))`,\n\t\t`$mutexName = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encode(mirrorMutex)}'))`,\n\t\t`$expoArguments = @([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encode(JSON.stringify(args))}')) | ConvertFrom-Json)`,\n\t\t`$gradleArguments = @([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encode(JSON.stringify(gradleArguments))}')) | ConvertFrom-Json)`,\n\t\t'$env:ANDROID_HOME = $androidHome',\n\t\t'$env:ANDROID_SDK_ROOT = $androidHome',\n\t\t`$env:NODE_ENV = '${mode === 'development' ? 'development' : 'production'}'`,\n\t\t...(preferredDevice || mode === 'bundle'\n\t\t\t? []\n\t\t\t: [\"$env:ORG_GRADLE_PROJECT_reactNativeArchitectures = 'x86_64'\"]),\n\t\t'$mutex = [Threading.Mutex]::new($false, $mutexName)',\n\t\t'$mutexAcquired = $false',\n\t\t'try {',\n\t\t'try { $mutexAcquired = $mutex.WaitOne([TimeSpan]::FromMinutes(30)) } catch [Threading.AbandonedMutexException] { $mutexAcquired = $true }',\n\t\t\"if (-not $mutexAcquired) { throw 'Timed out waiting for another AbsoluteJS Expo Android build to release the managed Windows mirror.' }\",\n\t\t'New-Item -ItemType Directory -Force -Path $directory | Out-Null',\n\t\t`& robocopy.exe $source $directory /MIR /XD node_modules .expo .git .gradle .cxx .kotlin build /XF bun.lock bun.lockb .absolutejs-preserve-subst.cjs .absolutejs-source-lock.sha256 ${WINDOWS_BUILD_STAMP} /NFL /NDL /NJH /NJS /NP`,\n\t\t'$copyExit = $LASTEXITCODE',\n\t\t'if ($copyExit -ge 8) { exit $copyExit }',\n\t\t'Set-Location $directory',\n\t\t// Stamp this mirror as recently built, then evict sibling mirrors whose\n\t\t// last build (or creation, for mirrors without a stamp) is past retention.\n\t\t`$buildStamp = Join-Path $directory '${WINDOWS_BUILD_STAMP}'`,\n\t\t\"Set-Content $buildStamp ([DateTime]::UtcNow.ToString('o')) -NoNewline\",\n\t\t`$staleBefore = [DateTime]::UtcNow.AddDays(-${windowsBuildRetentionDays()})`,\n\t\t`Get-ChildItem -LiteralPath (Split-Path -Parent $directory) -Directory -ErrorAction SilentlyContinue | Where-Object { -not [String]::Equals($_.FullName, $directory, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path (Join-Path $_.FullName '.absolutejs-expo-project')) } | ForEach-Object { $stamp = Join-Path $_.FullName '${WINDOWS_BUILD_STAMP}'; $lastBuild = if (Test-Path $stamp) { (Get-Item $stamp).LastWriteTimeUtc } else { $_.LastWriteTimeUtc }; if ($lastBuild -lt $staleBefore) { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } }`,\n\t\t\"$sourceLock = @('bun.lock', 'bun.lockb') | ForEach-Object { Join-Path $source $_ } | Where-Object { Test-Path $_ } | Select-Object -First 1\",\n\t\t\"$sourceLockHash = if ($sourceLock) { (Get-FileHash $sourceLock -Algorithm SHA256).Hash } else { 'none' }\",\n\t\t\"$lockMarker = Join-Path $directory '.absolutejs-source-lock.sha256'\",\n\t\t\"$priorLockHash = if (Test-Path $lockMarker) { (Get-Content $lockMarker -Raw).Trim() } else { '' }\",\n\t\t\"if ($priorLockHash -ne $sourceLockHash) { Remove-Item (Join-Path $directory 'bun.lock'), (Join-Path $directory 'bun.lockb') -Force -ErrorAction SilentlyContinue; Set-Content $lockMarker $sourceLockHash -NoNewline }\",\n\t\t'$bun = (Get-Command bun.exe -ErrorAction Stop).Source',\n\t\t'& $bun install',\n\t\t'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }',\n\t\t'$mirrorRoot = Split-Path -Parent $directory',\n\t\t\"$staleDrives = @(& subst.exe) | ForEach-Object { if ($_ -match '^([A-Z]:)\\\\\\\\: => (.+)$' -and [String]::Equals($Matches[2].TrimEnd('\\\\'), $mirrorRoot.TrimEnd('\\\\'), [StringComparison]::OrdinalIgnoreCase)) { $Matches[1] } }\",\n\t\t'foreach ($staleDrive in $staleDrives) { & subst.exe $staleDrive /D | Out-Null }',\n\t\t\"$drive = @('Z:', 'Y:', 'X:', 'W:', 'V:', 'U:', 'T:') | Where-Object { -not (Test-Path ($_ + '\\\\')) } | Select-Object -First 1\",\n\t\t\"if (-not $drive) { throw 'AbsoluteJS could not reserve a temporary drive letter for the Expo Android build.' }\",\n\t\t'& subst.exe $drive $mirrorRoot',\n\t\t\"if ($LASTEXITCODE -ne 0) { throw 'AbsoluteJS could not create the short Expo Android build path.' }\",\n\t\t`try { ${mappedBuild} } finally { Set-Location ($env:SystemDrive + '\\\\'); & subst.exe $drive /D | Out-Null }`,\n\t\t'} finally {',\n\t\t'if ($mutexAcquired) { $mutex.ReleaseMutex() }',\n\t\t'$mutex.Dispose()',\n\t\t'}',\n\t\t'exit 0'\n\t].join('; ');\n\n\treturn [\n\t\t'powershell.exe',\n\t\t'-NoProfile',\n\t\t'-EncodedCommand',\n\t\tBuffer.from(source, 'utf16le').toString('base64')\n\t];\n};\n\nconst runAbsoluteExpoWindowsBuild = async (\n\tcommand: string[],\n\toptions: AbsoluteExpoWindowsBuildCommandOptions\n) => {\n\tconst child = Bun.spawn(command, {\n\t\tenv: options.env ?? process.env,\n\t\tsignal: options.signal,\n\t\tstderr: 'inherit',\n\t\tstdin: 'ignore',\n\t\tstdout: 'inherit'\n\t});\n\n\treturn child.exited;\n};\n\n/** Build the production Expo App Bundle in a Windows-local managed mirror.\n * The returned path is the same mirror viewed through WSL, so signing and the\n * immutable release pipeline continue on the caller side without copying the\n * artifact through stdout or a temporary share. */\nexport const buildAbsoluteExpoAndroidWslBundle = async (\n\toptions: BuildAbsoluteExpoAndroidWslBundleOptions\n) => {\n\tconst capture = options.capture ?? captureCommand;\n\tconst command = encodedWindowsExpoAndroidCommand(\n\t\toptions.project,\n\t\toptions.androidRoot,\n\t\t'',\n\t\t[],\n\t\tcapture,\n\t\t0,\n\t\tundefined,\n\t\t'bundle',\n\t\toptions.gradleArguments\n\t);\n\tconst exitCode = await (options.run ?? runAbsoluteExpoWindowsBuild)(\n\t\tcommand,\n\t\t{\n\t\t\tenv: options.env ?? process.env,\n\t\t\tsignal: options.signal\n\t\t}\n\t);\n\tif (exitCode !== 0) {\n\t\tthrow new Error(\n\t\t\t`Expo Android Windows release build exited with status ${exitCode}.`\n\t\t);\n\t}\n\n\treturn join(\n\t\tabsoluteExpoWindowsBuildDirectory(options.project, options.androidRoot),\n\t\t'android',\n\t\t'app',\n\t\t'build',\n\t\t'outputs',\n\t\t'bundle',\n\t\t'release',\n\t\t'app-release.aab'\n\t);\n};\n\nconst commandEnvironment = (options: PlanAbsoluteExpoDevOptions) => ({\n\tABSOLUTE_EXPO_DEVELOPMENT: '1',\n\tNODE_ENV: 'development',\n\t...(options.certificateAuthorityPath\n\t\t? {\n\t\t\t\tABSOLUTE_EXPO_DEVELOPMENT_CA_PATH:\n\t\t\t\t\toptions.certificateAuthorityPath\n\t\t\t}\n\t\t: {}),\n\t...(options.androidOrigin\n\t\t? { EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN: options.androidOrigin }\n\t\t: {}),\n\t...(options.iosOrigin\n\t\t? { EXPO_PUBLIC_ABSOLUTE_DEV_IOS_ORIGIN: options.iosOrigin }\n\t\t: {}),\n\t...(options.metroHost\n\t\t? { REACT_NATIVE_PACKAGER_HOSTNAME: options.metroHost }\n\t\t: {})\n});\n\nexport const absoluteExpoExecutable = async (project: string) => {\n\tconst executable = join(project, 'node_modules', '.bin', 'expo');\n\ttry {\n\t\tawait access(executable);\n\n\t\treturn executable;\n\t} catch {\n\t\tthrow new TypeError(\n\t\t\t'Expo dependencies are not installed in the generated shell. Run `absolute mobile init --yes`.'\n\t\t);\n\t}\n};\n\nexport const planAbsoluteExpoDevSession = (\n\tconfig: NormalizedAbsoluteMobileConfig,\n\toptions: PlanAbsoluteExpoDevOptions\n): AbsoluteExpoDevPlan => {\n\tif (config.engine !== 'expo')\n\t\tthrow new TypeError('The Expo development controller requires Expo.');\n\tif (options.platforms.length === 0 && options.metro === 'external')\n\t\tthrow new TypeError(\n\t\t\t'External Expo execution requires a target platform.'\n\t\t);\n\tconst secureOrigin = [options.androidOrigin, options.iosOrigin].some(\n\t\t(origin) => origin && new URL(origin).protocol === 'https:'\n\t);\n\tif (secureOrigin && !options.certificateAuthorityPath)\n\t\tthrow new TypeError(\n\t\t\t'Expo HTTPS development requires the AbsoluteJS development CA certificate.'\n\t\t);\n\tconst env = commandEnvironment(options);\n\tconst metro: AbsoluteExpoDevCommand = {\n\t\targs: [\n\t\t\t'start',\n\t\t\t'--dev-client',\n\t\t\t'--host',\n\t\t\t'localhost',\n\t\t\t'--port',\n\t\t\tString(options.metroPort)\n\t\t],\n\t\tenv,\n\t\trole: 'metro'\n\t};\n\tconst prepare: AbsoluteExpoDevCommand | undefined =\n\t\toptions.platforms.length === 0\n\t\t\t? undefined\n\t\t\t: {\n\t\t\t\t\targs: [\n\t\t\t\t\t\t'prebuild',\n\t\t\t\t\t\t'--clean',\n\t\t\t\t\t\t'--no-install',\n\t\t\t\t\t\t'--platform',\n\t\t\t\t\t\toptions.platforms.length === 2\n\t\t\t\t\t\t\t? 'all'\n\t\t\t\t\t\t\t: (options.platforms[0] ?? 'all')\n\t\t\t\t\t],\n\t\t\t\t\tenv,\n\t\t\t\t\trole: 'native-prepare'\n\t\t\t\t};\n\tconst native = options.platforms.map((platform) => {\n\t\tconst device =\n\t\t\tplatform === 'android' ? options.androidDevice : options.iosDevice;\n\t\tconst command: AbsoluteExpoDevCommand = {\n\t\t\targs: [\n\t\t\t\t`run:${platform}`,\n\t\t\t\t'--no-bundler',\n\t\t\t\t...(device ? ['--device', device] : [])\n\t\t\t],\n\t\t\tenv,\n\t\t\tplatform,\n\t\t\trole: 'native-build'\n\t\t};\n\n\t\treturn command;\n\t});\n\n\treturn {\n\t\tcommands: [\n\t\t\t...(prepare ? [prepare] : []),\n\t\t\t...(options.metro === 'external' ? [] : [metro]),\n\t\t\t...native\n\t\t],\n\t\tmetroPort: options.metroPort,\n\t\tproject: config.nativeProjectDirectory\n\t};\n};\n\nconst abortError = () =>\n\tnew DOMException('Expo development aborted.', 'AbortError');\n\nconst forwardLines = (\n\tprocess: ChildProcess,\n\tonLine: (line: string) => void\n) => {\n\tconst attach = (stream: NodeJS.ReadableStream | null) => {\n\t\tif (!stream) return;\n\t\tlet buffered = '';\n\t\tstream.on('data', (chunk: Buffer) => {\n\t\t\tbuffered += chunk.toString('utf8');\n\t\t\tlet newline = buffered.indexOf('\\n');\n\t\t\twhile (newline >= 0) {\n\t\t\t\tonLine(buffered.slice(0, newline).replace(/\\r$/u, ''));\n\t\t\t\tbuffered = buffered.slice(newline + 1);\n\t\t\t\tnewline = buffered.indexOf('\\n');\n\t\t\t}\n\t\t});\n\t\tstream.on('end', () => {\n\t\t\tif (buffered) onLine(buffered);\n\t\t});\n\t};\n\tattach(process.stdout);\n\tattach(process.stderr);\n};\n\nconst stopProcess = async (process: ChildProcess) => {\n\tif (process.exitCode !== null || process.killed) return;\n\tprocess.kill('SIGTERM');\n\tawait Promise.race([\n\t\tnew Promise<void>((resolve) => process.once('exit', () => resolve())),\n\t\tnew Promise<void>((resolve) =>\n\t\t\tsetTimeout(resolve, PROCESS_CLOSE_TIMEOUT_MS)\n\t\t)\n\t]);\n\tif (process.exitCode === null) process.kill('SIGKILL');\n};\n\nconst waitForExit = (process: ChildProcess) =>\n\tnew Promise<number>((resolve) => {\n\t\tif (process.exitCode !== null) {\n\t\t\tresolve(process.exitCode);\n\n\t\t\treturn;\n\t\t}\n\t\tprocess.once('exit', (code) => resolve(code ?? 1));\n\t});\n\ntype ExpoUtilityCommandOptions = {\n\tcwd: string;\n\tenv?: NodeJS.ProcessEnv;\n\tsignal?: AbortSignal;\n\tlog: (message: string) => void;\n};\n\nconst runUtilityCommand = async (\n\trun: typeof spawn,\n\tcommand: string,\n\targs: string[],\n\toptions: ExpoUtilityCommandOptions\n) => {\n\tconst child = run(command, args, {\n\t\tcwd: options.cwd,\n\t\tenv: options.env ?? process.env,\n\t\tstdio: ['ignore', 'pipe', 'pipe']\n\t});\n\tforwardLines(child, options.log);\n\tconst abort = () => void stopProcess(child);\n\toptions.signal?.addEventListener('abort', abort, { once: true });\n\tconst exitCode = await waitForExit(child);\n\toptions.signal?.removeEventListener('abort', abort);\n\tif (options.signal?.aborted) throw abortError();\n\n\treturn exitCode;\n};\n\nexport const installAbsoluteExpoAndroidRelease = async (\n\toptions: InstallAbsoluteExpoAndroidReleaseOptions\n): Promise<AbsoluteExpoAndroidReleaseInstallation> => {\n\tif (options.config.engine !== 'expo')\n\t\tthrow new TypeError(\n\t\t\t'The Expo Android release installer requires Expo.'\n\t\t);\n\tif (!options.config.platforms.includes('android'))\n\t\tthrow new TypeError(\n\t\t\t'The Expo Android release installer requires the android platform.'\n\t\t);\n\tconst host = options.host ?? detectAbsoluteMobileHost();\n\tconst project = options.config.nativeProjectDirectory;\n\tconst executable =\n\t\toptions.executable ?? (await absoluteExpoExecutable(project));\n\tconst capture = options.capture ?? captureCommand;\n\tconst run = options.spawnProcess ?? spawn;\n\tconst log = options.log ?? (() => undefined);\n\tconst env: NodeJS.ProcessEnv = { ...process.env, NODE_ENV: 'production' };\n\tconst utilityOptions: ExpoUtilityCommandOptions = {\n\t\tcwd: project,\n\t\tenv,\n\t\tlog,\n\t\tsignal: options.signal\n\t};\n\tconst started = performance.now();\n\tconst prebuildExit = await runUtilityCommand(\n\t\trun,\n\t\texecutable,\n\t\t['prebuild', '--clean', '--no-install', '--platform', 'android'],\n\t\tutilityOptions\n\t);\n\tif (prebuildExit !== 0)\n\t\tthrow new Error(\n\t\t\t`Expo Android production preparation exited with status ${prebuildExit}.`\n\t\t);\n\tconst androidRoot =\n\t\toptions.androidRoot ??\n\t\tprocess.env.ANDROID_HOME ??\n\t\tprocess.env.ANDROID_SDK_ROOT ??\n\t\tabsoluteManagedAndroidSdkRoot(host);\n\tconst args = [\n\t\t'run:android',\n\t\t'--variant',\n\t\t'release',\n\t\t'--no-bundler',\n\t\t...(options.androidDevice ? ['--device', options.androidDevice] : [])\n\t];\n\tconst invocation =\n\t\thost === 'wsl'\n\t\t\t? encodedWindowsExpoAndroidCommand(\n\t\t\t\t\tproject,\n\t\t\t\t\tandroidRoot,\n\t\t\t\t\toptions.config.appId,\n\t\t\t\t\targs,\n\t\t\t\t\tcapture,\n\t\t\t\t\toptions.forwardedPort,\n\t\t\t\t\toptions.androidDevice,\n\t\t\t\t\t'release'\n\t\t\t\t)\n\t\t\t: [executable, ...args];\n\tif (host === 'wsl')\n\t\tlog(\n\t\t\t'[android] Mirroring native inputs to the Windows host for an installable Expo release build.'\n\t\t);\n\tconst [releaseExecutable, ...releaseArguments] = invocation;\n\tif (!releaseExecutable)\n\t\tthrow new Error('Expo Android release build command is empty.');\n\tconst releaseExit = await runUtilityCommand(\n\t\trun,\n\t\treleaseExecutable,\n\t\treleaseArguments,\n\t\tutilityOptions\n\t);\n\tif (releaseExit !== 0)\n\t\tthrow new Error(\n\t\t\t`Expo Android release build exited with status ${releaseExit}.`\n\t\t);\n\tconst adb =\n\t\toptions.androidAdb ??\n\t\tjoin(\n\t\t\tandroidRoot,\n\t\t\t'platform-tools',\n\t\t\thost === 'windows' || host === 'wsl' ? 'adb.exe' : 'adb'\n\t\t);\n\tconst serial =\n\t\thost === 'wsl'\n\t\t\t? (options.androidDevice ??\n\t\t\t\tcapture([adb, 'devices'])\n\t\t\t\t\t.stdout.split(/\\r?\\n/u)\n\t\t\t\t\t.flatMap((line) => {\n\t\t\t\t\t\tconst match = /^(\\S+)\\s+device$/u.exec(line.trim());\n\n\t\t\t\t\t\treturn match?.[1] ? [match[1]] : [];\n\t\t\t\t\t})\n\t\t\t\t\t.find((value) => value.startsWith('emulator-')))\n\t\t\t: launchLocalExpoAndroidRelease(\n\t\t\t\t\tadb,\n\t\t\t\t\toptions.config.appId,\n\t\t\t\t\toptions.forwardedPort,\n\t\t\t\t\tcapture,\n\t\t\t\t\toptions.androidDevice\n\t\t\t\t);\n\tif (!serial)\n\t\tthrow new Error(\n\t\t\t'Expo Android release build completed but its device could not be identified.'\n\t\t);\n\n\treturn { durationMs: performance.now() - started, serial };\n};\nexport const installAbsoluteExpoIosRelease = async (\n\toptions: InstallAbsoluteExpoIosReleaseOptions\n): Promise<AbsoluteExpoIosReleaseInstallation> => {\n\tif (options.config.engine !== 'expo')\n\t\tthrow new TypeError('The Expo iOS release installer requires Expo.');\n\tif (!options.config.platforms.includes('ios'))\n\t\tthrow new TypeError(\n\t\t\t'The Expo iOS release installer requires the ios platform.'\n\t\t);\n\tconst project = options.config.nativeProjectDirectory;\n\tconst executable =\n\t\toptions.executable ?? (await absoluteExpoExecutable(project));\n\tconst capture = options.capture ?? captureCommand;\n\tconst run = options.spawnProcess ?? spawn;\n\tconst log = options.log ?? (() => undefined);\n\tconst xcrun = options.xcrun ?? 'xcrun';\n\tconst env: NodeJS.ProcessEnv = { ...process.env, NODE_ENV: 'production' };\n\tconst utilityOptions: ExpoUtilityCommandOptions = {\n\t\tcwd: project,\n\t\tenv,\n\t\tlog,\n\t\tsignal: options.signal\n\t};\n\tconst bootedSimulators = () => {\n\t\tconst inventory = capture([\n\t\t\txcrun,\n\t\t\t'simctl',\n\t\t\t'list',\n\t\t\t'devices',\n\t\t\t'booted',\n\t\t\t'-j'\n\t\t]);\n\t\tif (inventory.exitCode !== 0)\n\t\t\tthrow new Error(\n\t\t\t\t'Could not inspect booted iOS Simulators. Run `absolute mobile doctor ios --fix`.'\n\t\t\t);\n\n\t\treturn parseBootedAbsoluteExpoIosSimulators(inventory.stdout);\n\t};\n\tconst started = performance.now();\n\tconst prebuildExit = await runUtilityCommand(\n\t\trun,\n\t\texecutable,\n\t\t['prebuild', '--clean', '--no-install', '--platform', 'ios'],\n\t\tutilityOptions\n\t);\n\tif (prebuildExit !== 0)\n\t\tthrow new Error(\n\t\t\t`Expo iOS production preparation exited with status ${prebuildExit}.`\n\t\t);\n\tconst beforeBuild = bootedSimulators();\n\tconst selectedBeforeBuild = options.iosDevice\n\t\t? beforeBuild.find((udid) => udid === options.iosDevice)\n\t\t: beforeBuild[0];\n\tif (options.iosDevice && !selectedBeforeBuild)\n\t\tthrow new Error('The selected Expo iOS Simulator is not booted.');\n\tconst releaseExit = await runUtilityCommand(\n\t\trun,\n\t\texecutable,\n\t\t[\n\t\t\t'run:ios',\n\t\t\t'--configuration',\n\t\t\t'Release',\n\t\t\t'--no-bundler',\n\t\t\t...(selectedBeforeBuild ? ['--device', selectedBeforeBuild] : [])\n\t\t],\n\t\tutilityOptions\n\t);\n\tif (releaseExit !== 0)\n\t\tthrow new Error(\n\t\t\t`Expo iOS release build exited with status ${releaseExit}.`\n\t\t);\n\tconst udid = selectedBeforeBuild ?? bootedSimulators()[0];\n\tif (!udid)\n\t\tthrow new Error(\n\t\t\t'Expo iOS release build completed but its Simulator could not be identified.'\n\t\t);\n\tconst launched = capture([\n\t\txcrun,\n\t\t'simctl',\n\t\t'launch',\n\t\t'--terminate-running-process',\n\t\tudid,\n\t\toptions.config.appId\n\t]);\n\tif (launched.exitCode !== 0)\n\t\tthrow new Error('Expo iOS release app launch failed.');\n\tlog(`Installed and launched the Expo iOS release app on ${udid}.`);\n\n\treturn { durationMs: performance.now() - started, udid };\n};\nexport const parseBootedAbsoluteExpoIosSimulators = (source: string) => {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(source);\n\t} catch {\n\t\tthrow new TypeError('Invalid iOS Simulator inventory JSON.');\n\t}\n\tif (!parsed || typeof parsed !== 'object')\n\t\tthrow new TypeError('Invalid iOS Simulator inventory JSON.');\n\tconst devices = Reflect.get(parsed, 'devices');\n\tif (!devices || typeof devices !== 'object') return [];\n\n\treturn Object.values(devices).flatMap((values) => {\n\t\tif (!Array.isArray(values)) return [];\n\n\t\treturn values.flatMap((value) => {\n\t\t\tif (!value || typeof value !== 'object') return [];\n\t\t\tconst udid = Reflect.get(value, 'udid');\n\t\t\tconst state = Reflect.get(value, 'state');\n\t\t\tconst isAvailable = Reflect.get(value, 'isAvailable');\n\n\t\t\treturn typeof udid === 'string' &&\n\t\t\t\tstate === 'Booted' &&\n\t\t\t\tisAvailable !== false\n\t\t\t\t? [udid]\n\t\t\t\t: [];\n\t\t});\n\t});\n};\nexport const startAbsoluteExpoDevSession = async (\n\toptions: StartAbsoluteExpoDevOptions\n) => {\n\tlet plan = planAbsoluteExpoDevSession(options.config, options);\n\tconst executable =\n\t\toptions.executable ?? (await absoluteExpoExecutable(plan.project));\n\tconst run = options.spawnProcess ?? spawn;\n\tconst host = options.host ?? detectAbsoluteMobileHost();\n\tconst capture = options.capture ?? captureCommand;\n\tconst log = options.log ?? (() => undefined);\n\tconst timings: Partial<Record<AbsoluteExpoDevState, number>> = {};\n\tlet caEnrollmentServer: AbsoluteIosCaEnrollmentServer | null = null;\n\tconst closeEnrollmentServer = async () => {\n\t\tconst server = caEnrollmentServer;\n\t\tcaEnrollmentServer = null;\n\t\tawait server?.close();\n\t};\n\tconst publishTiming = (\n\t\tphase: AbsoluteExpoDevState,\n\t\tdurationMs: number,\n\t\tdetails: Pick<AbsoluteExpoDevPhaseTiming, 'reason' | 'retryCount'> = {}\n\t) => {\n\t\ttimings[phase] = (timings[phase] ?? 0) + durationMs;\n\t\toptions.onPhaseTiming?.({ durationMs, phase, ...details });\n\t};\n\tconst setState = (state: AbsoluteExpoDevState) => {\n\t\toptions.onStateChange?.(state);\n\t};\n\tif (options.signal?.aborted) throw abortError();\n\tconst prepareCommand = plan.commands.find(\n\t\t(command) => command.role === 'native-prepare'\n\t);\n\tlet metroCommand = plan.commands.find(\n\t\t(command) => command.role === 'metro'\n\t);\n\tlet nativeCommands = plan.commands.filter(\n\t\t(command) => command.role === 'native-build'\n\t);\n\tconst runPrepareCommand = async (command: AbsoluteExpoDevCommand) => {\n\t\tsetState('preparing-native');\n\t\tconst prepareStarted = performance.now();\n\t\tconst prepareProcess = run(executable, command.args, {\n\t\t\tcwd: plan.project,\n\t\t\tenv: { ...process.env, ...command.env },\n\t\t\tstdio: ['ignore', 'pipe', 'pipe']\n\t\t});\n\t\tforwardLines(prepareProcess, (line) => {\n\t\t\tif (line) log(`[prebuild] ${line}`);\n\t\t});\n\t\tconst abortPrepare = () => void stopProcess(prepareProcess);\n\t\toptions.signal?.addEventListener('abort', abortPrepare, { once: true });\n\t\tconst prepareExit = await waitForExit(prepareProcess);\n\t\toptions.signal?.removeEventListener('abort', abortPrepare);\n\t\tif (options.signal?.aborted) throw abortError();\n\t\tif (prepareExit !== 0) {\n\t\t\tsetState('failed');\n\t\t\tthrow new Error(\n\t\t\t\t`Expo native preparation exited with status ${prepareExit}.`\n\t\t\t);\n\t\t}\n\t\tpublishTiming('preparing-native', performance.now() - prepareStarted);\n\t};\n\tif (prepareCommand) await runPrepareCommand(prepareCommand);\n\tconst managedMetro = metroCommand !== undefined;\n\tif (managedMetro) setState('starting-metro');\n\tconst metroStarted = performance.now();\n\tlet metro: ChildProcess | undefined;\n\tlet metroRetryCount = 0;\n\tconst attemptedMetroPorts = [plan.metroPort];\n\tconst waitForMetroReady = (child: ChildProcess) =>\n\t\tnew Promise<void>((resolve, reject) => {\n\t\t\tlet ready = false;\n\t\t\tlet conflict = false;\n\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\treject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t'Expo Metro did not become ready within 120 seconds.'\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}, METRO_READY_TIMEOUT_MS);\n\t\t\tforwardLines(child, (line) => {\n\t\t\t\tif (line) log(`[metro] ${line}`);\n\t\t\t\tif (metroPortConflict(line)) conflict = true;\n\t\t\t\tif (\n\t\t\t\t\t!ready &&\n\t\t\t\t\t/(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(\n\t\t\t\t\t\tline\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tready = true;\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t});\n\t\t\tchild.once('exit', (code) => {\n\t\t\t\tif (ready) return;\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new MetroStartupError(code ?? 1, conflict));\n\t\t\t});\n\t\t});\n\tconst startManagedMetro = async (): Promise<void> => {\n\t\tif (!metroCommand) return;\n\t\tif (options.signal?.aborted) throw abortError();\n\t\tmetro = run(executable, metroCommand.args, {\n\t\t\tcwd: plan.project,\n\t\t\tenv: { ...process.env, ...metroCommand.env },\n\t\t\tstdio: ['ignore', 'pipe', 'pipe']\n\t\t});\n\t\ttry {\n\t\t\tawait waitForMetroReady(metro);\n\t\t} catch (error) {\n\t\t\tif (\n\t\t\t\t!(error instanceof MetroStartupError) ||\n\t\t\t\t!error.conflict ||\n\t\t\t\tattemptedMetroPorts.length >= METRO_START_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tawait stopProcess(metro);\n\t\t\tconst allocate =\n\t\t\t\toptions.allocateMetroPort ?? allocateAvailableMetroPort;\n\t\t\tconst replacementPort = await allocate(attemptedMetroPorts);\n\t\t\tif (\n\t\t\t\t!Number.isSafeInteger(replacementPort) ||\n\t\t\t\treplacementPort < 1 ||\n\t\t\t\treplacementPort > MAX_TCP_PORT ||\n\t\t\t\tattemptedMetroPorts.includes(replacementPort)\n\t\t\t) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'Could not allocate a distinct replacement Metro port.',\n\t\t\t\t\t{ cause: error }\n\t\t\t\t);\n\t\t\t}\n\t\t\tattemptedMetroPorts.push(replacementPort);\n\t\t\tmetroRetryCount += 1;\n\t\t\tlog(\n\t\t\t\t'Metro port was claimed during startup; retrying automatically on a new port.'\n\t\t\t);\n\t\t\tplan = planAbsoluteExpoDevSession(options.config, {\n\t\t\t\t...options,\n\t\t\t\tmetroPort: replacementPort\n\t\t\t});\n\t\t\tmetroCommand = plan.commands.find(\n\t\t\t\t(command) => command.role === 'metro'\n\t\t\t);\n\t\t\tnativeCommands = plan.commands.filter(\n\t\t\t\t(command) => command.role === 'native-build'\n\t\t\t);\n\n\t\t\tawait startManagedMetro();\n\t\t}\n\t};\n\tconst abort = () => {\n\t\tif (metro) void stopProcess(metro);\n\t};\n\toptions.signal?.addEventListener('abort', abort, { once: true });\n\tconst runNativeCommand = async (command: AbsoluteExpoDevCommand) => {\n\t\tif (options.signal?.aborted) throw abortError();\n\t\tconst { platform } = command;\n\t\tif (!platform)\n\t\t\tthrow new TypeError(\n\t\t\t\t'Expo native build command is missing a platform.'\n\t\t\t);\n\t\tconst state: AbsoluteExpoDevState =\n\t\t\tplatform === 'android' ? 'building-android' : 'building-ios';\n\t\tsetState(state);\n\t\tconst started = performance.now();\n\t\tconst invocation =\n\t\t\tplatform === 'android' && host === 'wsl'\n\t\t\t\t? encodedWindowsExpoAndroidCommand(\n\t\t\t\t\t\tplan.project,\n\t\t\t\t\t\toptions.androidRoot ??\n\t\t\t\t\t\t\tabsoluteManagedAndroidSdkRoot(host),\n\t\t\t\t\t\toptions.config.appId,\n\t\t\t\t\t\tcommand.args,\n\t\t\t\t\t\tcapture,\n\t\t\t\t\t\tplan.metroPort,\n\t\t\t\t\t\toptions.androidDevice\n\t\t\t\t\t)\n\t\t\t\t: [executable, ...command.args];\n\t\tif (platform === 'android' && host === 'wsl') {\n\t\t\tlog(\n\t\t\t\t'[android] Mirroring native inputs to the Windows host for accelerated Expo build and launch.'\n\t\t\t);\n\t\t}\n\t\tconst [nativeExecutable, ...nativeArguments] = invocation;\n\t\tif (!nativeExecutable)\n\t\t\tthrow new Error('Expo native build command is empty.');\n\t\tconst child = run(nativeExecutable, nativeArguments, {\n\t\t\tcwd: plan.project,\n\t\t\tenv: { ...process.env, ...command.env },\n\t\t\tstdio: ['ignore', 'pipe', 'pipe']\n\t\t});\n\t\tforwardLines(child, (line) => {\n\t\t\tif (line) log(`[${platform}] ${line}`);\n\t\t});\n\t\tconst abortChild = () => void stopProcess(child);\n\t\toptions.signal?.addEventListener('abort', abortChild, { once: true });\n\t\tconst exitCode = await waitForExit(child);\n\t\toptions.signal?.removeEventListener('abort', abortChild);\n\t\tif (options.signal?.aborted) throw abortError();\n\t\tif (exitCode !== 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`Expo ${platform} development build exited with status ${exitCode}.`\n\t\t\t);\n\t\t}\n\t\tif (platform === 'android' && host !== 'wsl') {\n\t\t\tconst androidRoot =\n\t\t\t\toptions.androidRoot ??\n\t\t\t\tprocess.env.ANDROID_HOME ??\n\t\t\t\tprocess.env.ANDROID_SDK_ROOT ??\n\t\t\t\tabsoluteManagedAndroidSdkRoot(host);\n\t\t\tconst adb =\n\t\t\t\toptions.androidAdb ??\n\t\t\t\tjoin(\n\t\t\t\t\tandroidRoot,\n\t\t\t\t\t'platform-tools',\n\t\t\t\t\thost === 'windows' ? 'adb.exe' : 'adb'\n\t\t\t\t);\n\t\t\tconnectLocalExpoAndroid(\n\t\t\t\tadb,\n\t\t\t\toptions.config.appId,\n\t\t\t\tplan.metroPort,\n\t\t\t\tcapture,\n\t\t\t\toptions.androidDevice\n\t\t\t);\n\t\t\tlog('Connected the Expo Android development client to Metro.');\n\t\t}\n\t\tif (\n\t\t\tplatform === 'ios' &&\n\t\t\toptions.certificateAuthorityPath &&\n\t\t\toptions.iosOrigin &&\n\t\t\tnew URL(options.iosOrigin).protocol === 'https:' &&\n\t\t\t!options.iosDevice\n\t\t) {\n\t\t\tsetState('enrolling-trust');\n\t\t\tconst trustStarted = performance.now();\n\t\t\tconst utilityOptions: ExpoUtilityCommandOptions = {\n\t\t\t\tcwd: plan.project,\n\t\t\t\tsignal: options.signal,\n\t\t\t\tlog: (line: string) => line && log(`[ios-trust] ${line}`)\n\t\t\t};\n\t\t\tconst trustExit = await runUtilityCommand(\n\t\t\t\trun,\n\t\t\t\t'xcrun',\n\t\t\t\t[\n\t\t\t\t\t'simctl',\n\t\t\t\t\t'keychain',\n\t\t\t\t\t'booted',\n\t\t\t\t\t'add-root-cert',\n\t\t\t\t\toptions.certificateAuthorityPath\n\t\t\t\t],\n\t\t\t\tutilityOptions\n\t\t\t);\n\t\t\tif (trustExit !== 0)\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Expo iOS Simulator development CA trust exited with status ${trustExit}.`\n\t\t\t\t);\n\t\t\tawait runUtilityCommand(\n\t\t\t\trun,\n\t\t\t\t'xcrun',\n\t\t\t\t['simctl', 'terminate', 'booted', options.config.appId],\n\t\t\t\tutilityOptions\n\t\t\t);\n\t\t\tconst launchExit = await runUtilityCommand(\n\t\t\t\trun,\n\t\t\t\t'xcrun',\n\t\t\t\t['simctl', 'launch', 'booted', options.config.appId],\n\t\t\t\tutilityOptions\n\t\t\t);\n\t\t\tif (launchExit !== 0)\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Expo iOS Simulator relaunch exited with status ${launchExit}.`\n\t\t\t\t);\n\t\t\tlog(\n\t\t\t\t'Installed the AbsoluteJS development CA into the Expo iOS Simulator and relaunched the app.'\n\t\t\t);\n\t\t\tpublishTiming('enrolling-trust', performance.now() - trustStarted);\n\t\t}\n\t\tif (platform === 'ios' && !options.iosDevice) {\n\t\t\tconst utilityOptions: ExpoUtilityCommandOptions = {\n\t\t\t\tcwd: plan.project,\n\t\t\t\tsignal: options.signal,\n\t\t\t\tlog: (line: string) => line && log(`[ios-connect] ${line}`)\n\t\t\t};\n\t\t\tawait runUtilityCommand(\n\t\t\t\trun,\n\t\t\t\t'xcrun',\n\t\t\t\t['simctl', 'terminate', 'booted', options.config.appId],\n\t\t\t\tutilityOptions\n\t\t\t);\n\t\t\tconst connectExit = await runUtilityCommand(\n\t\t\t\trun,\n\t\t\t\t'xcrun',\n\t\t\t\t[\n\t\t\t\t\t'simctl',\n\t\t\t\t\t'openurl',\n\t\t\t\t\t'booted',\n\t\t\t\t\texpoDevelopmentClientUrl(\n\t\t\t\t\t\toptions.config.appId,\n\t\t\t\t\t\toptions.metroHost ?? 'localhost',\n\t\t\t\t\t\tplan.metroPort\n\t\t\t\t\t)\n\t\t\t\t],\n\t\t\t\tutilityOptions\n\t\t\t);\n\t\t\tif (connectExit !== 0)\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Expo iOS Simulator Metro connection exited with status ${connectExit}.`\n\t\t\t\t);\n\t\t\tlog(\n\t\t\t\t'Connected the Expo iOS Simulator development client to Metro.'\n\t\t\t);\n\t\t}\n\t\tconst durationMs = performance.now() - started;\n\t\ttimings[state] = durationMs;\n\t\toptions.onPhaseTiming?.({ durationMs, phase: state });\n\t};\n\tconst runNativeCommands = async (\n\t\tcommands: AbsoluteExpoDevCommand[]\n\t): Promise<void> => {\n\t\tconst [command, ...remaining] = commands;\n\t\tif (!command) return;\n\t\tawait runNativeCommand(command);\n\t\tawait runNativeCommands(remaining);\n\t};\n\tconst startPhysicalIosEnrollment = async () => {\n\t\tif (\n\t\t\t!options.iosDevice ||\n\t\t\t!options.certificateAuthorityPath ||\n\t\t\t!options.iosOrigin ||\n\t\t\tnew URL(options.iosOrigin).protocol !== 'https:'\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tsetState('enrolling-trust');\n\t\tconst trustStarted = performance.now();\n\t\tconst startEnrollment =\n\t\t\toptions.startCaEnrollmentServer ??\n\t\t\tstartAbsoluteIosCaEnrollmentServer;\n\t\tcaEnrollmentServer = await startEnrollment({\n\t\t\tcertificateAuthorityPath: options.certificateAuthorityPath,\n\t\t\tdisplayHost: new URL(options.iosOrigin).hostname\n\t\t});\n\t\tlog(\n\t\t\t`On the iOS device, open ${caEnrollmentServer.url}, install the AbsoluteJS development CA profile, then enable it under Settings > General > About > Certificate Trust Settings. This public CA endpoint exists only for this dev session.`\n\t\t);\n\t\tpublishTiming('enrolling-trust', performance.now() - trustStarted);\n\t};\n\ttry {\n\t\tawait startPhysicalIosEnrollment();\n\t\tawait startManagedMetro();\n\t\tif (managedMetro)\n\t\t\tpublishTiming('starting-metro', performance.now() - metroStarted, {\n\t\t\t\t...(metroRetryCount > 0\n\t\t\t\t\t? {\n\t\t\t\t\t\t\treason: 'metro-port-conflict' as const,\n\t\t\t\t\t\t\tretryCount: metroRetryCount\n\t\t\t\t\t\t}\n\t\t\t\t\t: {})\n\t\t\t});\n\t\tawait runNativeCommands(nativeCommands);\n\t\tsetState('ready');\n\n\t\tlet closed = false;\n\t\tlet rebuildQueue = Promise.resolve();\n\t\tconst session: AbsoluteExpoDevSession = {\n\t\t\tmetroPort: plan.metroPort,\n\t\t\tplatforms: options.platforms,\n\t\t\ttimings,\n\t\t\tclose: async () => {\n\t\t\t\tif (closed) return;\n\t\t\t\tclosed = true;\n\t\t\t\toptions.signal?.removeEventListener('abort', abort);\n\t\t\t\tawait rebuildQueue;\n\t\t\t\tif (metro) await stopProcess(metro);\n\t\t\t\tawait closeEnrollmentServer();\n\t\t\t\tsetState('closed');\n\t\t\t},\n\t\t\trebuild: () => {\n\t\t\t\tconst rebuild = rebuildQueue.then(async () => {\n\t\t\t\t\tif (closed)\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'The Expo development session is closed.'\n\t\t\t\t\t\t);\n\t\t\t\t\tif (prepareCommand) await runPrepareCommand(prepareCommand);\n\t\t\t\t\tawait runNativeCommands(nativeCommands);\n\t\t\t\t\tsetState('ready');\n\n\t\t\t\t\treturn session;\n\t\t\t\t});\n\t\t\t\trebuildQueue = rebuild.then(\n\t\t\t\t\t() => undefined,\n\t\t\t\t\t() => undefined\n\t\t\t\t);\n\n\t\t\t\treturn rebuild;\n\t\t\t}\n\t\t};\n\n\t\treturn session;\n\t} catch (error) {\n\t\toptions.signal?.removeEventListener('abort', abort);\n\t\tif (metro) await stopProcess(metro);\n\t\tawait closeEnrollmentServer();\n\t\tsetState('failed');\n\t\tthrow error;\n\t}\n};\n",
|
package/dist/react/browser.js
CHANGED
|
@@ -73,7 +73,7 @@ var __esm = (fn, res, err) => () => {
|
|
|
73
73
|
var __require = import.meta.require;
|
|
74
74
|
|
|
75
75
|
// src/core/islandManifest.ts
|
|
76
|
-
var toIslandFrameworkSegment = (framework) => framework[0]?.toUpperCase() + framework.slice(1), collectFrameworkIslands = (manifest, prefix) => {
|
|
76
|
+
var ISLAND_BOOTSTRAP_MANIFEST_KEY = "AbsoluteIslandBootstrap", LEGACY_ISLAND_BOOTSTRAP_MANIFEST_KEY = "BootstrapClient", toIslandFrameworkSegment = (framework) => framework[0]?.toUpperCase() + framework.slice(1), collectFrameworkIslands = (manifest, prefix) => {
|
|
77
77
|
const entries = {};
|
|
78
78
|
let found = false;
|
|
79
79
|
for (const [key, value] of Object.entries(manifest)) {
|
|
@@ -86,7 +86,7 @@ var toIslandFrameworkSegment = (framework) => framework[0]?.toUpperCase() + fram
|
|
|
86
86
|
found = true;
|
|
87
87
|
}
|
|
88
88
|
return found ? entries : undefined;
|
|
89
|
-
}, getIslandManifestEntries = (manifest) => {
|
|
89
|
+
}, getIslandBootstrapPath = (manifest) => manifest[ISLAND_BOOTSTRAP_MANIFEST_KEY] ?? manifest[LEGACY_ISLAND_BOOTSTRAP_MANIFEST_KEY], getIslandManifestEntries = (manifest) => {
|
|
90
90
|
const islands = {};
|
|
91
91
|
const frameworks = ["react", "svelte", "vue", "angular"];
|
|
92
92
|
for (const framework of frameworks) {
|
|
@@ -320,5 +320,5 @@ export {
|
|
|
320
320
|
useIslandStore
|
|
321
321
|
};
|
|
322
322
|
|
|
323
|
-
//# debugId=
|
|
323
|
+
//# debugId=C2905FF3BE08A60664756E2164756E21
|
|
324
324
|
//# sourceMappingURL=browser.js.map
|