peachsim 0.1.2199 → 0.1.2201

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.
Files changed (107) hide show
  1. package/cli/commands/inspect/settling.ts +20 -20
  2. package/cli/outbound-endpoints.ts +12 -0
  3. package/dist-lib/agent-daemon-client.cjs +1 -1
  4. package/dist-lib/agent-events.cjs +1 -1
  5. package/dist-lib/attached-projects.cjs +1 -1
  6. package/dist-lib/auth/shared-session.cjs +1 -1
  7. package/dist-lib/backend-origin.cjs +1 -1
  8. package/dist-lib/beta.cjs +1 -1
  9. package/dist-lib/beta.mjs +1 -1
  10. package/dist-lib/bridge-constants.cjs +1 -1
  11. package/dist-lib/bridge-contract-input.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.mjs +1 -1
  13. package/dist-lib/bridge-contract.cjs +1 -1
  14. package/dist-lib/bridge-contract.mjs +1 -1
  15. package/dist-lib/bundle-channel.cjs +1423 -1118
  16. package/dist-lib/capture-contract.cjs +1 -1
  17. package/dist-lib/capture-contract.mjs +1 -1
  18. package/dist-lib/cloud-contract.cjs +1 -1
  19. package/dist-lib/cloud-contract.mjs +1 -1
  20. package/dist-lib/cloud-test-args.cjs +1 -1
  21. package/dist-lib/cloud-test-args.mjs +1 -1
  22. package/dist-lib/cloud.cjs +1 -1
  23. package/dist-lib/cloud.mjs +1 -1
  24. package/dist-lib/config.cjs +1 -1
  25. package/dist-lib/config.mjs +1 -1
  26. package/dist-lib/detox/index.cjs +22 -22
  27. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  28. package/dist-lib/expo-swift-bundle.cjs +45945 -40026
  29. package/dist-lib/expo-swift-metadata.cjs +1 -1
  30. package/dist-lib/expo-swift-metadata.mjs +1 -1
  31. package/dist-lib/home-paths.cjs +1 -1
  32. package/dist-lib/host/bridge-host.cjs +50879 -49774
  33. package/dist-lib/host/fetch-proxy-handler.cjs +16 -4
  34. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  36. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  37. package/dist-lib/host/websocket-proxy.cjs +1 -1
  38. package/dist-lib/index.cjs +36 -24
  39. package/dist-lib/jump-to-source-babel.cjs +1 -1
  40. package/dist-lib/jump-to-source-native.cjs +1 -1
  41. package/dist-lib/kotlin-host.cjs +100 -10
  42. package/dist-lib/menu.cjs +1 -1
  43. package/dist-lib/menu.mjs +1 -1
  44. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  46. package/dist-lib/metro-production-bundle.cjs +1 -1
  47. package/dist-lib/metro-production-bundle.mjs +1 -1
  48. package/dist-lib/metro.cjs +16 -4
  49. package/dist-lib/native-app-identity-project.cjs +14 -2
  50. package/dist-lib/native-app-identity-project.mjs +14 -2
  51. package/dist-lib/native-app-identity.cjs +1 -1
  52. package/dist-lib/native-app-identity.mjs +1 -1
  53. package/dist-lib/native-firebase-metadata.cjs +1 -1
  54. package/dist-lib/native-firebase-metadata.mjs +1 -1
  55. package/dist-lib/pdfium-raster.cjs +1 -1
  56. package/dist-lib/pdfium-raster.mjs +1 -1
  57. package/dist-lib/plist.cjs +1 -1
  58. package/dist-lib/plist.mjs +1 -1
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/public-origins-lib-esm.json +24 -24
  62. package/dist-lib/public-origins-lib.json +302 -289
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/reap-browser.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +21 -21
  68. package/dist-lib/sdk.mjs +21 -21
  69. package/dist-lib/skills.cjs +3520 -2920
  70. package/dist-lib/swift-compile.cjs +34 -3
  71. package/dist-lib/swift-compile.mjs +34 -3
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +1 -1
  75. package/dist-lib/swift-submit.mjs +1 -1
  76. package/dist-lib/swift-toolchain.cjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +42 -8
  79. package/dist-lib/vite.cjs +16 -4
  80. package/dist-lib/worklet-executable.cjs +1 -1
  81. package/dist-lib/worklet-executable.mjs +1 -1
  82. package/dist-types/packages/peach-engine/src/engine/scene-table.d.ts +2 -0
  83. package/dist-types/packages/peach-engine/src/engine/yoga-layout.d.ts +2 -1
  84. package/dist-types/packages/peach-engine/src/render-worker/app-prefetch-contract.d.ts +5 -0
  85. package/dist-types/packages/peach-engine/src/render-worker/protocol.d.ts +2 -0
  86. package/dist-types/packages/peach-engine/src/render-worker/tree-sync-port.d.ts +5 -0
  87. package/kotlin/sdk.tar.gz +0 -0
  88. package/package.json +1 -1
  89. package/runtime-companion.json +1 -1
  90. package/sdk/ios/System/Library/Frameworks/QuartzCore.framework/Headers/CAValueFunction.h +1 -1
  91. package/sdk/ios/System/Library/Frameworks/QuartzCore.framework/Modules/module.modulemap +1 -0
  92. package/sdk/ios/System/Library/Frameworks/UIKitText.framework/Headers/UIKitText.apinotes +14 -0
  93. package/src/android-layout-ids.ts +50 -0
  94. package/src/android-view-classes.ts +41 -0
  95. package/src/expo-swift-host.ts +6 -1
  96. package/src/java-module-abi.ts +2 -0
  97. package/src/javaAbiContract.ts +2 -0
  98. package/src/kotlin-app.ts +33 -7
  99. package/src/kotlin-host.ts +83 -6
  100. package/src/kotlin-maven-sources.ts +248 -0
  101. package/src/native-prebuilt.ts +3 -3
  102. package/src/react-native-core-build.ts +176 -0
  103. package/src/react-native-core-digest.ts +1 -1
  104. package/src/react-native-core.ts +24 -7
  105. package/src/swift-standin-emit.ts +9 -1
  106. package/src/swift-wasm-prune.ts +41 -3
  107. package/src/turbo-compile.ts +6 -4
package/src/kotlin-app.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import fs from 'node:fs'
7
7
  import path from 'node:path'
8
8
  import { readAndroidDrawables, type AndroidDrawable } from './android-drawables'
9
+ import { readAndroidLayoutIds, type AndroidLayoutIds } from './android-layout-ids'
9
10
  import { readAndroidStrings, type AndroidStringResources } from './android-strings'
10
11
  import {
11
12
  readResourceValues,
@@ -18,6 +19,7 @@ import { parseXml, type XmlElement } from './android-xml'
18
19
  import frameworkThemes from './generated/android-framework-themes.json' with { type: 'json' }
19
20
  import { isRecord } from './is-record'
20
21
  import { composeBomVersions } from './kotlin-compose-bom'
22
+ import { mavenKotlinSources } from './kotlin-maven-sources'
21
23
  import { KotlinVersionCatalog } from './kotlin-version-catalog'
22
24
  import type { AndroidGradleDependency } from './androidGradleContract'
23
25
 
@@ -60,6 +62,7 @@ export interface KotlinAppProject {
60
62
  strings: AndroidStringResources
61
63
  colors: KotlinAppColor[]
62
64
  drawables: AndroidDrawable[]
65
+ layoutIds: AndroidLayoutIds
63
66
  }
64
67
 
65
68
  // a color resource's ARGB in the day and the night configuration
@@ -244,6 +247,7 @@ function kotlinPluginVersion(
244
247
  }
245
248
 
246
249
  async function checkModuleGradle(
250
+ projectDir: string,
247
251
  gradle: { file: string; text: string },
248
252
  pins: AndroidPins,
249
253
  catalog: KotlinVersionCatalog,
@@ -258,7 +262,7 @@ async function checkModuleGradle(
258
262
  throw new Error(`${where}: ${value} is not a literal or catalog dependency`)
259
263
  }
260
264
  const platforms = new Map<string, string>()
261
- const dependencies: { coordinate: string; where: string }[] = []
265
+ const dependencies: { coordinate: string; where: string; compile: boolean }[] = []
262
266
  let usesSerialization = false
263
267
  for (const match of gradle.text.matchAll(
264
268
  /\bid\(\s*"([^"]+)"\s*\)|\bkotlin\(\s*"([^"]+)"\s*\)|\balias\(\s*libs\.plugins\.([\w.]+)\s*\)/g,
@@ -291,6 +295,7 @@ async function checkModuleGradle(
291
295
  dependencies.push({
292
296
  coordinate: `${dependency.group}:${dependency.name}:${dependency.version}`,
293
297
  where: gradle.file,
298
+ compile: true,
294
299
  })
295
300
  }
296
301
  } else
@@ -321,6 +326,10 @@ async function checkModuleGradle(
321
326
  if (!call) throw new Error(`${where}: ${text} is not a literal dependency`)
322
327
  const [, configuration = '', value = ''] = call
323
328
  if (/^(test|androidTest|debug|release)/.test(configuration)) continue
329
+ const compile =
330
+ configuration === 'implementation' ||
331
+ configuration === 'api' ||
332
+ configuration === 'compileOnly'
324
333
  const platform = /^platform\(\s*(.*?)\s*\)$/.exec(
325
334
  platformVariables.get(value) ?? value,
326
335
  )
@@ -338,11 +347,13 @@ async function checkModuleGradle(
338
347
  }
339
348
  } else {
340
349
  for (const coordinate of coordinates(value, where))
341
- dependencies.push({ coordinate, where })
350
+ dependencies.push({ coordinate, where, compile })
342
351
  }
343
352
  }
344
353
  }
345
- for (const { coordinate, where } of dependencies) {
354
+ const librarySources: string[] = []
355
+ const seen = new Set<string>()
356
+ for (const { coordinate, where, compile } of dependencies) {
346
357
  const parts = /^([^:]+):([^:]+)(?::([^:]+))?$/.exec(coordinate)
347
358
  if (!parts)
348
359
  throw new Error(`${where}: ${coordinate} is not a group:artifact[:version]`)
@@ -361,10 +372,22 @@ async function checkModuleGradle(
361
372
  throw new Error(
362
373
  `${where}: ${name}:${version} differs from Peach's ${pin} pin ${pins[pin]}`,
363
374
  )
364
- } else if (!PLATFORM_GROUPS.has(group) && !PLATFORM_ARTIFACTS.has(name))
365
- throw new Error(`${where}: ${name} is not in Peach's Kotlin closure`)
375
+ } else if (!PLATFORM_GROUPS.has(group) && !PLATFORM_ARTIFACTS.has(name)) {
376
+ const sources = compile
377
+ ? await mavenKotlinSources(projectDir, { group, artifact, version }, where)
378
+ : null
379
+ if (!sources)
380
+ throw new Error(
381
+ `${where}: ${group}:${artifact}:${version} is not in Peach's Kotlin closure`,
382
+ )
383
+ for (const file of sources) {
384
+ if (seen.has(file)) continue
385
+ seen.add(file)
386
+ librarySources.push(file)
387
+ }
388
+ }
366
389
  }
367
- return { usesSerialization }
390
+ return { usesSerialization, librarySources }
368
391
  }
369
392
 
370
393
  // Resources.selectDefaultTheme: the theme a context without one gets for its
@@ -566,7 +589,8 @@ export async function readKotlinApp(
566
589
  targetSdk: model.targetSdk,
567
590
  }
568
591
  })()
569
- const { usesSerialization } = await checkModuleGradle(
592
+ const { usesSerialization, librarySources } = await checkModuleGradle(
593
+ projectDir,
570
594
  gradle,
571
595
  options.pins,
572
596
  catalog,
@@ -729,6 +753,7 @@ export async function readKotlinApp(
729
753
  }
730
754
  walk(dir)
731
755
  }
756
+ sources.push(...librarySources)
732
757
  if (sources.length === 0)
733
758
  throw new Error(`${moduleDir}/src/main holds no Kotlin sources`)
734
759
  const resources = {
@@ -757,5 +782,6 @@ export async function readKotlinApp(
757
782
  strings: readAndroidStrings(moduleDir),
758
783
  colors: readAppColors(moduleDir, resources),
759
784
  drawables: readAndroidDrawables(moduleDir, resources),
785
+ layoutIds: readAndroidLayoutIds(moduleDir),
760
786
  }
761
787
  }
@@ -18,6 +18,7 @@ import {
18
18
  type BridgeModule,
19
19
  type BridgeSelection,
20
20
  } from './android-bridge-packages'
21
+ import { layoutViewClasses } from './android-view-classes'
21
22
  import { rnxHomeDir } from './home-paths'
22
23
  import { isRecord } from './is-record'
23
24
  import {
@@ -39,6 +40,7 @@ import {
39
40
  type ViewModelClass,
40
41
  } from './kotlin-view-models'
41
42
  import type { DayNight, DrawableFile } from './android-drawables'
43
+ import type { XmlElement } from './android-xml'
42
44
  import type { ExpoKotlinModules } from './expo-kotlin-host'
43
45
  import type { KotlinAppProject, KotlinWindowTheme } from './kotlin-app'
44
46
  import type { KotlinSdkProfile, KotlinToolchain } from './kotlinSdkContract'
@@ -968,7 +970,7 @@ function appResourceSources(app: KotlinAppProject): Record<string, string> {
968
970
  .map(([locale, value]) => `${literal(locale)} to ${render(value)}`)
969
971
  .join(', ')})`,
970
972
  }))
971
- const kinds = [
973
+ const tableKinds = [
972
974
  {
973
975
  type: 'string',
974
976
  table: 'peachAppStrings',
@@ -1007,6 +1009,16 @@ function appResourceSources(app: KotlinAppProject): Record<string, string> {
1007
1009
  .join(',\n')}\n)`,
1008
1010
  })),
1009
1011
  },
1012
+ // a layout is its element tree; one only in a qualified directory has none
1013
+ {
1014
+ type: 'layout',
1015
+ table: 'peachAppLayouts',
1016
+ value: 'LayoutNode?',
1017
+ entries: app.layoutIds.layouts.map((name) => {
1018
+ const element = app.layoutIds.elements.get(name)
1019
+ return { name, init: element ? layoutInit(element) : 'null' }
1020
+ }),
1021
+ },
1010
1022
  // the day and the night ARGB
1011
1023
  {
1012
1024
  type: 'color',
@@ -1017,19 +1029,38 @@ function appResourceSources(app: KotlinAppProject): Record<string, string> {
1017
1029
  init: `intArrayOf(${argb(color.day)}, ${argb(color.night)})`,
1018
1030
  })),
1019
1031
  },
1020
- ].map((kind, index) => ({
1032
+ ]
1033
+ // ids are named in R and in the name table; they hold no Resources value
1034
+ const nameOnly = [{ type: 'id', names: app.layoutIds.ids }]
1035
+ const numbered = <Kind extends { entries: Array<{ name: string }> }>(
1036
+ kind: Kind,
1037
+ index: number,
1038
+ ): Omit<Kind, 'entries'> & {
1039
+ entries: Array<Kind['entries'][number] & { id: string }>
1040
+ } => ({
1021
1041
  ...kind,
1022
1042
  entries: kind.entries.map((entry, number) => ({
1023
1043
  ...entry,
1024
1044
  id: id(index + 1, number),
1025
1045
  })),
1026
- }))
1046
+ })
1047
+ const kinds = tableKinds.map((kind, index) => numbered(kind, index))
1048
+ const viewClasses = layoutViewClasses(app.layoutIds.elements.values())
1049
+ const rKinds = [
1050
+ ...kinds,
1051
+ ...nameOnly.map((kind, index) =>
1052
+ numbered(
1053
+ { type: kind.type, entries: kind.names.map((name) => ({ name })) },
1054
+ kinds.length + index,
1055
+ ),
1056
+ ),
1057
+ ]
1027
1058
  return {
1028
1059
  'R.kt': [
1029
1060
  `package ${app.namespace}`,
1030
1061
  '',
1031
1062
  'object R {',
1032
- ...kinds.flatMap((kind) => [
1063
+ ...rKinds.flatMap((kind) => [
1033
1064
  ` object ${kind.type} {`,
1034
1065
  ...kind.entries.map(
1035
1066
  (entry) => ` const val ${field(entry.name)}: Int = ${entry.id}`,
@@ -1051,6 +1082,10 @@ function appResourceSources(app: KotlinAppProject): Record<string, string> {
1051
1082
  'androidx.compose.ui.graphics.vector.ImageVector',
1052
1083
  'androidx.compose.ui.graphics.vector.addPathNodes',
1053
1084
  'androidx.compose.ui.unit.Dp',
1085
+ 'android.content.Context',
1086
+ 'android.util.AttributeSet',
1087
+ 'android.view.LayoutNode',
1088
+ 'android.view.View',
1054
1089
  ].map((name) => `import ${name}`),
1055
1090
  '',
1056
1091
  ...kinds.flatMap((kind) => [
@@ -1059,16 +1094,45 @@ function appResourceSources(app: KotlinAppProject): Record<string, string> {
1059
1094
  ')',
1060
1095
  '',
1061
1096
  ]),
1097
+ 'internal val peachAppViews: Map<String, (Context, AttributeSet?) -> View> = mapOf(',
1098
+ ...viewClasses.map(
1099
+ (name) =>
1100
+ ` ${literal(name)} to { context, attrs -> ${name}(context, attrs) },`,
1101
+ ),
1102
+ ')',
1103
+ '',
1104
+ `internal val peachAppNames: Map<Int, String> = mapOf(`,
1105
+ ...rKinds.flatMap((kind) =>
1106
+ kind.entries.map(
1107
+ (entry) =>
1108
+ ` ${entry.id} to ${literal(`${app.packageName}:${kind.type}/${entry.name}`)},`,
1109
+ ),
1110
+ ),
1111
+ ')',
1112
+ '',
1062
1113
  'internal val compiledAppResources = PeachAppResources(',
1063
1114
  ...kinds.map(
1064
1115
  (kind) =>
1065
1116
  ` ${kind.table.slice('peachApp'.length).toLowerCase()} = ${kind.table},`,
1066
1117
  ),
1118
+ ' views = peachAppViews,',
1119
+ ' names = peachAppNames,',
1067
1120
  ')',
1068
1121
  ].join('\n'),
1069
1122
  }
1070
1123
  }
1071
1124
 
1125
+ // the elements a layout inflates, without the namespace declarations and
1126
+ // tools attributes aapt2 strips
1127
+ function layoutInit(element: XmlElement): string {
1128
+ const attributes = Object.entries(element.attributes)
1129
+ .filter(([key]) => !key.startsWith('xmlns') && !key.startsWith('tools:'))
1130
+ .map(([key, value]) => `${literal(key)} to ${literal(value)}`)
1131
+ return `LayoutNode(${literal(element.name)}, mapOf(${attributes.join(', ')}), listOf(${element.children
1132
+ .map(layoutInit)
1133
+ .join(', ')}))`
1134
+ }
1135
+
1072
1136
  // a drawable file's table entry: a vector as the ImageVector.Builder calls
1073
1137
  // XmlVectorParser makes, its colors chosen by the configuration's night
1074
1138
  // mode; a bitmap as its encoded bytes
@@ -1171,11 +1235,19 @@ async function compileKotlin(
1171
1235
  const sdk = await loadKotlinSdk(moduleRoot, toolchain)
1172
1236
  const plugin = path.join(sdk.root, 'plugin.jar')
1173
1237
  const sdkLibrary = path.join(sdk.root, `${sdkProfile}.klib`)
1174
- const java = javaSpecs.length
1238
+ const javaRead = javaSpecs.length
1175
1239
  ? await readJavaModuleSpecs(javaSpecs, sdk.root, await ensureToolchain(toolchain))
1176
1240
  : null
1241
+ const java =
1242
+ javaRead && javaRead.specs.some((spec) => !spec.application)
1243
+ ? { ...javaRead, specs: javaRead.specs.filter((spec) => !spec.application) }
1244
+ : null
1245
+ const applicationJava = javaRead?.specs.filter((spec) => spec.application) ?? []
1177
1246
  const generated = {
1178
1247
  ...sourceGenerated,
1248
+ ...Object.fromEntries(
1249
+ applicationJava.map((spec, index) => [`JavaApplication${index}.kt`, spec.source]),
1250
+ ),
1179
1251
  'PeachBindingsInitialization.kt': [
1180
1252
  '@file:OptIn(kotlin.js.ExperimentalJsExport::class)',
1181
1253
  'package dev.peach.jni',
@@ -1209,7 +1281,7 @@ async function compileKotlin(
1209
1281
  sdk: sdk.sha256,
1210
1282
  sdkProfile,
1211
1283
  usesSerialization,
1212
- java: java?.sha256,
1284
+ java: javaRead?.sha256,
1213
1285
  }),
1214
1286
  )
1215
1287
  for (const { file, bytes } of inputs)
@@ -1442,6 +1514,11 @@ async function compileKotlin(
1442
1514
  : ''
1443
1515
  for (const [name, source] of Object.entries(origins))
1444
1516
  output = output.replaceAll(path.join(generatedDir, name), source)
1517
+ for (const [index, spec] of applicationJava.entries())
1518
+ output = output.replaceAll(
1519
+ path.join(generatedDir, `JavaApplication${index}.kt`),
1520
+ spec.file,
1521
+ )
1445
1522
  for (const { file, target } of compiledSources)
1446
1523
  output = output
1447
1524
  .replaceAll(target, file)
@@ -0,0 +1,248 @@
1
+ // a dependency outside Peach's pinned closure compiles from the sources jar
2
+ // the app's Maven repositories publish for that coordinate
3
+ import { createHash } from 'node:crypto'
4
+ import fs from 'node:fs'
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+ import { inflateRawSync } from 'node:zlib'
8
+
9
+ const pending = new Map<string, Promise<string[]>>()
10
+
11
+ function braceBlock(text: string, from: number): { body: string; end: number } | null {
12
+ const open = text.indexOf('{', from)
13
+ if (open < 0) return null
14
+ let depth = 0
15
+ for (let index = open; index < text.length; index++) {
16
+ const char = text[index]
17
+ if (char === '{') depth++
18
+ else if (char === '}') {
19
+ depth--
20
+ if (depth === 0) return { body: text.slice(open + 1, index), end: index }
21
+ }
22
+ }
23
+ return null
24
+ }
25
+
26
+ function repositoryRoot(value: string, where: string): string {
27
+ if (value.startsWith('https://')) return value.replace(/\/+$/, '')
28
+ if (value.startsWith('/')) return value.replace(/\/+$/, '')
29
+ if (value.startsWith('file:')) {
30
+ const pathname = new URL(value).pathname
31
+ if (!pathname.startsWith('/'))
32
+ throw new Error(`${where}: Maven repository ${value} is not a local path`)
33
+ return decodeURIComponent(pathname).replace(/\/+$/, '')
34
+ }
35
+ throw new Error(
36
+ `${where}: Maven repository ${value} is not an https URL or a local path`,
37
+ )
38
+ }
39
+
40
+ // google(), mavenCentral(), mavenLocal(), or one literal maven URL
41
+ function withoutComments(text: string): string {
42
+ let out = ''
43
+ for (let index = 0; index < text.length; index++) {
44
+ const char = text[index]
45
+ const next = text[index + 1]
46
+ if (char === '"' || char === "'") {
47
+ const quote = char
48
+ out += char
49
+ index++
50
+ while (index < text.length && text[index] !== quote) {
51
+ out += text[index]
52
+ if (text[index] === '\\') {
53
+ index++
54
+ out += text[index] ?? ''
55
+ }
56
+ index++
57
+ }
58
+ out += text[index] ?? ''
59
+ continue
60
+ }
61
+ if (char === '/' && next === '/') {
62
+ while (index < text.length && text[index] !== '\n') index++
63
+ out += '\n'
64
+ continue
65
+ }
66
+ if (char === '/' && next === '*') {
67
+ index += 2
68
+ while (index < text.length && !(text[index] === '*' && text[index + 1] === '/'))
69
+ index++
70
+ index++
71
+ continue
72
+ }
73
+ out += char
74
+ }
75
+ return out
76
+ }
77
+
78
+ function repositories(body: string, where: string): string[] {
79
+ const stripped = withoutComments(body)
80
+ const roots: string[] = []
81
+ const leftover = stripped.replace(
82
+ /\b(google|mavenCentral|mavenLocal)\s*\(\s*\)|\bmaven\s*(?:\(\s*["']([^"']+)["']\s*\)|\{([^{}]*)\})/g,
83
+ (match, name: string, direct: string | undefined, block: string | undefined) => {
84
+ if (name === 'google') roots.push('https://dl.google.com/dl/android/maven2')
85
+ else if (name === 'mavenCentral') roots.push('https://repo1.maven.org/maven2')
86
+ else if (name === 'mavenLocal')
87
+ roots.push(path.join(os.homedir(), '.m2', 'repository'))
88
+ else {
89
+ const declared = direct ?? /["']([^"']+)["']/.exec(block ?? '')?.[1]
90
+ if (!declared)
91
+ throw new Error(`${where}: ${match.trim()} declares no repository URL`)
92
+ roots.push(repositoryRoot(declared, where))
93
+ }
94
+ return ''
95
+ },
96
+ )
97
+ if (leftover.replace(/[\s;]/g, ''))
98
+ throw new Error(`${where}: ${leftover.trim()} is not a literal Maven repository`)
99
+ return roots
100
+ }
101
+
102
+ function mavenRepositories(projectDir: string, where: string): string[] {
103
+ const file = ['settings.gradle.kts', 'settings.gradle']
104
+ .map((name) => path.join(projectDir, name))
105
+ .find((candidate) => fs.existsSync(candidate))
106
+ if (!file) throw new Error(`${where}: ${projectDir} has no settings.gradle`)
107
+ const text = fs.readFileSync(file, 'utf8')
108
+ const management = text.search(/\bdependencyResolutionManagement\b/)
109
+ if (management < 0)
110
+ throw new Error(`${where}: ${file} declares no dependency repositories`)
111
+ const managed = braceBlock(text, management)
112
+ if (!managed)
113
+ throw new Error(`${where}: ${file} dependencyResolutionManagement block never closes`)
114
+ const repositoriesAt = managed.body.search(/\brepositories\b/)
115
+ if (repositoriesAt < 0)
116
+ throw new Error(`${where}: ${file} declares no dependency repositories`)
117
+ const block = braceBlock(managed.body, repositoriesAt)
118
+ if (!block) throw new Error(`${where}: ${file} repositories block never closes`)
119
+ const roots = repositories(block.body, `${where}: ${file}`)
120
+ if (roots.length === 0)
121
+ throw new Error(`${where}: ${file} declares no dependency repositories`)
122
+ return roots
123
+ }
124
+
125
+ function zipSources(bytes: Buffer, where: string): { name: string; data: Buffer }[] {
126
+ let end = -1
127
+ const earliest = Math.max(0, bytes.length - 22 - 0xffff)
128
+ for (let index = bytes.length - 22; index >= earliest; index--) {
129
+ if (bytes.readUInt32LE(index) === 0x06054b50) {
130
+ end = index
131
+ break
132
+ }
133
+ }
134
+ if (end < 0) throw new Error(`${where}: sources jar is not a zip`)
135
+ const count = bytes.readUInt16LE(end + 8)
136
+ if (count !== bytes.readUInt16LE(end + 10))
137
+ throw new Error(`${where}: sources jar spans zip disks`)
138
+ let offset = bytes.readUInt32LE(end + 16)
139
+ if (offset === 0xffffffff) throw new Error(`${where}: sources jar is zip64`)
140
+ const entries: { name: string; data: Buffer }[] = []
141
+ for (let index = 0; index < count; index++) {
142
+ if (bytes.readUInt32LE(offset) !== 0x02014b50)
143
+ throw new Error(`${where}: sources jar has a broken central directory`)
144
+ const flags = bytes.readUInt16LE(offset + 8)
145
+ const method = bytes.readUInt16LE(offset + 10)
146
+ const compressed = bytes.readUInt32LE(offset + 20)
147
+ const nameLength = bytes.readUInt16LE(offset + 28)
148
+ const extraLength = bytes.readUInt16LE(offset + 30)
149
+ const commentLength = bytes.readUInt16LE(offset + 32)
150
+ const localOffset = bytes.readUInt32LE(offset + 42)
151
+ const name = bytes.toString('utf8', offset + 46, offset + 46 + nameLength)
152
+ offset += 46 + nameLength + extraLength + commentLength
153
+ if (name.endsWith('/')) continue
154
+ if (flags & 1) throw new Error(`${where}: ${name} is encrypted`)
155
+ if (name.startsWith('/') || name.split('/').includes('..'))
156
+ throw new Error(`${where}: ${name} escapes the sources jar`)
157
+ if (bytes.readUInt32LE(localOffset) !== 0x04034b50)
158
+ throw new Error(`${where}: ${name} has no local header`)
159
+ const localName = bytes.readUInt16LE(localOffset + 26)
160
+ const localExtra = bytes.readUInt16LE(localOffset + 28)
161
+ const start = localOffset + 30 + localName + localExtra
162
+ const payload = bytes.subarray(start, start + compressed)
163
+ const data =
164
+ method === 0 ? Buffer.from(payload) : method === 8 ? inflateRawSync(payload) : null
165
+ if (!data) throw new Error(`${where}: ${name} uses zip method ${method}`)
166
+ entries.push({ name, data })
167
+ }
168
+ return entries
169
+ }
170
+
171
+ function extract(bytes: Buffer, where: string): Promise<string[]> {
172
+ const key = createHash('sha256').update(bytes).digest('hex')
173
+ const cached = pending.get(key)
174
+ if (cached) return cached
175
+ const request = (async () => {
176
+ const dest = path.join(os.tmpdir(), 'peach-maven-sources', key)
177
+ const marker = path.join(dest, '.complete')
178
+ if (!fs.existsSync(marker)) {
179
+ const entries = zipSources(bytes, where)
180
+ const java = entries.find((entry) => entry.name.endsWith('.java'))
181
+ if (java) throw new Error(`${where}: ${java.name} is a Java source`)
182
+ const kotlin = entries.filter((entry) => entry.name.endsWith('.kt'))
183
+ if (kotlin.length === 0) throw new Error(`${where}: sources jar has no Kotlin`)
184
+ const staging = `${dest}.partial-${process.pid}`
185
+ fs.rmSync(staging, { recursive: true, force: true })
186
+ for (const entry of kotlin) {
187
+ const file = path.join(staging, entry.name)
188
+ fs.mkdirSync(path.dirname(file), { recursive: true })
189
+ fs.writeFileSync(file, entry.data)
190
+ }
191
+ fs.writeFileSync(path.join(staging, '.complete'), '')
192
+ try {
193
+ fs.renameSync(staging, dest)
194
+ } catch (error) {
195
+ fs.rmSync(staging, { recursive: true, force: true })
196
+ if (!fs.existsSync(marker)) throw error
197
+ }
198
+ }
199
+ return fs
200
+ .readdirSync(dest, { recursive: true })
201
+ .flatMap((entry) => {
202
+ const name = String(entry)
203
+ return name.endsWith('.kt') ? [path.join(dest, name)] : []
204
+ })
205
+ .sort()
206
+ })().catch((error: unknown) => {
207
+ pending.delete(key)
208
+ throw error
209
+ })
210
+ pending.set(key, request)
211
+ return request
212
+ }
213
+
214
+ async function sourcesJar(
215
+ root: string,
216
+ group: string,
217
+ artifact: string,
218
+ version: string,
219
+ ): Promise<Buffer | null> {
220
+ const parts = [
221
+ ...group.split('.'),
222
+ artifact,
223
+ version,
224
+ `${artifact}-${version}-sources.jar`,
225
+ ]
226
+ if (root.startsWith('https://')) {
227
+ const url = `${root}/${parts.join('/')}`
228
+ const response = await fetch(url)
229
+ if (response.status === 404) return null
230
+ if (!response.ok) throw new Error(`${url} answered ${response.status}`)
231
+ return Buffer.from(await response.arrayBuffer())
232
+ }
233
+ const file = path.join(root, ...parts)
234
+ return fs.existsSync(file) ? fs.readFileSync(file) : null
235
+ }
236
+
237
+ export async function mavenKotlinSources(
238
+ projectDir: string,
239
+ coordinate: { group: string; artifact: string; version: string },
240
+ where: string,
241
+ ): Promise<string[] | null> {
242
+ const { group, artifact, version } = coordinate
243
+ for (const root of mavenRepositories(projectDir, where)) {
244
+ const jar = await sourcesJar(root, group, artifact, version)
245
+ if (jar) return extract(jar, `${where}: ${group}:${artifact}:${version}`)
246
+ }
247
+ return null
248
+ }
@@ -58,7 +58,7 @@ export function nativeEntryKey(inputs: Record<string, string>): string {
58
58
  export const nativeEntryPrefix = (kind: NativeEntryKind, name: string, key: string) =>
59
59
  `native/${kind}/${name}/${key}`
60
60
 
61
- const entryCache = (kind: NativeEntryKind, name: string, key: string) =>
61
+ export const nativeEntryCache = (kind: NativeEntryKind, name: string, key: string) =>
62
62
  path.join(peachCacheDir(), 'native-prebuilt', kind, name, key)
63
63
 
64
64
  // one GET; a missing object is null. a network failure is a miss too, since a
@@ -111,7 +111,7 @@ export function fetchNativeEntry(
111
111
  name: string,
112
112
  key: string,
113
113
  ): string | null {
114
- const dir = entryCache(kind, name, key)
114
+ const dir = nativeEntryCache(kind, name, key)
115
115
  const descriptorFile = path.join(dir, 'descriptor.json')
116
116
  if (fs.existsSync(descriptorFile)) return dir
117
117
  const base = `${NATIVE_PREBUILT_ORIGIN}/${nativeEntryPrefix(kind, name, key)}`
@@ -170,7 +170,7 @@ export function installNativeEntry(
170
170
  `native prebuilt ${kind}/${name}/${key}: ${file.path} does not match its descriptor`,
171
171
  )
172
172
  }
173
- const dir = entryCache(kind, name, key)
173
+ const dir = nativeEntryCache(kind, name, key)
174
174
  fs.mkdirSync(path.dirname(dir), { recursive: true })
175
175
  const staging = `${dir}.${process.pid}.partial`
176
176
  fs.rmSync(staging, { recursive: true, force: true })