peachsim 0.1.674 → 0.1.675

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 (81) hide show
  1. package/dist-lib/agent-daemon-client.cjs +1 -1
  2. package/dist-lib/agent-events.cjs +1 -1
  3. package/dist-lib/agent-identity.cjs +1 -1
  4. package/dist-lib/agent-sessions.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/capture-contract.cjs +1 -1
  16. package/dist-lib/capture-contract.mjs +1 -1
  17. package/dist-lib/cli-constants.cjs +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/detox/index.cjs +1 -1
  26. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  27. package/dist-lib/home-paths.cjs +1 -1
  28. package/dist-lib/host/bridge-host.cjs +1 -1
  29. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  30. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  31. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  32. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  33. package/dist-lib/host/websocket-proxy.cjs +1 -1
  34. package/dist-lib/index.cjs +1 -1
  35. package/dist-lib/jump-to-source-babel.cjs +1 -1
  36. package/dist-lib/jump-to-source-native.cjs +1 -1
  37. package/dist-lib/kotlin-host.cjs +78 -2
  38. package/dist-lib/menu.cjs +1 -1
  39. package/dist-lib/menu.mjs +1 -1
  40. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  41. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  42. package/dist-lib/metro-production-bundle.cjs +1 -1
  43. package/dist-lib/metro-production-bundle.mjs +1 -1
  44. package/dist-lib/metro.cjs +1 -1
  45. package/dist-lib/native-app-identity.cjs +1 -1
  46. package/dist-lib/native-app-identity.mjs +1 -1
  47. package/dist-lib/native-firebase-metadata.cjs +1 -1
  48. package/dist-lib/native-firebase-metadata.mjs +1 -1
  49. package/dist-lib/plist.cjs +1 -1
  50. package/dist-lib/plist.mjs +1 -1
  51. package/dist-lib/profiles.cjs +1 -1
  52. package/dist-lib/public-brand.cjs +1 -1
  53. package/dist-lib/react-native-host-modules.cjs +1 -1
  54. package/dist-lib/react-native-host-modules.mjs +1 -1
  55. package/dist-lib/render-mode.cjs +1 -1
  56. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  57. package/dist-lib/sdk.cjs +1 -1
  58. package/dist-lib/sdk.mjs +1 -1
  59. package/dist-lib/skills.cjs +1 -1
  60. package/dist-lib/swift-compile.cjs +1 -1
  61. package/dist-lib/swift-compile.mjs +1 -1
  62. package/dist-lib/swift-package.cjs +1 -1
  63. package/dist-lib/swift-package.mjs +1 -1
  64. package/dist-lib/swift-submit.cjs +1 -1
  65. package/dist-lib/swift-submit.mjs +1 -1
  66. package/dist-lib/swift.cjs +1 -1
  67. package/dist-lib/vite-swift.cjs +1 -1
  68. package/dist-lib/vite.cjs +1 -1
  69. package/kotlin/AppSources/android/content/Context.kt +4 -0
  70. package/kotlin/AppSources/android/content/res/Configuration.kt +4 -1
  71. package/kotlin/AppSources/android/content/res/Resources.kt +45 -1
  72. package/kotlin/AppSources/androidx/compose/ui/platform/AndroidCompositionLocals.kt +19 -0
  73. package/kotlin/AppSources/androidx/compose/ui/res/StringResources.kt +53 -0
  74. package/kotlin/AppSources/dev/peach/app/JavaFormat.kt +264 -0
  75. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +23 -7
  76. package/kotlin/fixtures/kotlin-app-variants/Strings.kt +49 -0
  77. package/package.json +1 -1
  78. package/src/android-strings.ts +163 -0
  79. package/src/android-xml.ts +3 -3
  80. package/src/kotlin-app.ts +4 -0
  81. package/src/kotlin-host.ts +98 -0
@@ -0,0 +1,163 @@
1
+ // an Android module's string resources as aapt2 compiles them: each
2
+ // <string>, <plurals> and <string-array> under res/values and its locale
3
+ // qualified directories, the text with its style tags removed (getString
4
+ // returns the unstyled text) and aapt2's StringBuilder rules applied. a
5
+ // qualifier Peach does not resolve yet, or a value that is a reference, is
6
+ // rejected by name, never read as the default or as text.
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { decodeXmlText } from './android-xml'
10
+
11
+ // one resource's value per locale: '' for values, then a language or a
12
+ // language and region as a BCP 47 tag (values-en-rGB is en-GB). the device
13
+ // resolves a whole plurals or array resource to one locale, never per item
14
+ export interface AndroidResource<Value> {
15
+ name: string
16
+ values: Record<string, Value>
17
+ }
18
+
19
+ export type PluralQuantity = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'
20
+
21
+ export interface AndroidStringResources {
22
+ strings: AndroidResource<string>[]
23
+ plurals: AndroidResource<Partial<Record<PluralQuantity, string>>>[]
24
+ arrays: AndroidResource<string[]>[]
25
+ }
26
+
27
+ const VALUES_DIR = /^values(?:-([a-z]{2,3})(?:-r([A-Z]{2}))?)?$/
28
+ const ELEMENT =
29
+ /<(string|plurals|string-array)(?=[\s/>])([^>]*?)(?:\/>|>([\s\S]*?)<\/\1\s*>)/g
30
+ const ITEM = /<item(?=[\s/>])([^>]*?)(?:\/>|>([\s\S]*?)<\/item\s*>)/g
31
+ const NAME = /\bname\s*=\s*"([^"]*)"/
32
+ const QUANTITY = /\bquantity\s*=\s*"([^"]*)"/
33
+ const QUANTITIES: readonly PluralQuantity[] = [
34
+ 'zero',
35
+ 'one',
36
+ 'two',
37
+ 'few',
38
+ 'many',
39
+ 'other',
40
+ ]
41
+
42
+ const isQuantity = (value: string): value is PluralQuantity =>
43
+ QUANTITIES.some((quantity) => quantity === value)
44
+
45
+ export function readAndroidStrings(moduleDir: string): AndroidStringResources {
46
+ const strings = new Map<string, Record<string, string>>()
47
+ const plurals = new Map<
48
+ string,
49
+ Record<string, Partial<Record<PluralQuantity, string>>>
50
+ >()
51
+ const arrays = new Map<string, Record<string, string[]>>()
52
+ const res = path.join(moduleDir, 'src/main/res')
53
+ const dirs = fs.existsSync(res) ? fs.readdirSync(res).sort() : []
54
+ for (const dir of dirs) {
55
+ if (!dir.startsWith('values')) continue
56
+ const qualifier = VALUES_DIR.exec(dir)
57
+ for (const entry of fs.readdirSync(path.join(res, dir)).sort()) {
58
+ if (!entry.endsWith('.xml')) continue
59
+ const file = path.join(res, dir, entry)
60
+ const text = fs.readFileSync(file, 'utf8').replace(/<!--[\s\S]*?-->/g, '')
61
+ for (const match of text.matchAll(ELEMENT)) {
62
+ const kind = match[1] ?? ''
63
+ const name = NAME.exec(match[2] ?? '')?.[1]
64
+ if (!name) throw new Error(`${file}: a <${kind}> without a name`)
65
+ if (!qualifier)
66
+ throw new Error(
67
+ `${file}: ${kind} ${name} under ${dir} is not resolved by peach kotlin yet (only values, values-<language> and values-<language>-r<REGION>)`,
68
+ )
69
+ const locale = qualifier[2]
70
+ ? `${qualifier[1]}-${qualifier[2]}`
71
+ : (qualifier[1] ?? '')
72
+ const body = match[3] ?? ''
73
+ const where = `${file}: ${kind} ${name}`
74
+ if (kind === 'string')
75
+ add(strings, name, locale, compileString(body, where), where)
76
+ else if (kind === 'plurals') {
77
+ const items: Partial<Record<PluralQuantity, string>> = {}
78
+ for (const item of body.matchAll(ITEM)) {
79
+ const quantity = QUANTITY.exec(item[1] ?? '')?.[1] ?? ''
80
+ if (!isQuantity(quantity))
81
+ throw new Error(`${where} has an item with quantity "${quantity}"`)
82
+ if (quantity in items) throw new Error(`${where} defines ${quantity} twice`)
83
+ items[quantity] = compileString(item[2] ?? '', `${where} ${quantity}`)
84
+ }
85
+ add(plurals, name, locale, items, where)
86
+ } else
87
+ add(
88
+ arrays,
89
+ name,
90
+ locale,
91
+ [...body.matchAll(ITEM)].map((item, index) =>
92
+ compileString(item[2] ?? '', `${where} item ${index}`),
93
+ ),
94
+ where,
95
+ )
96
+ }
97
+ }
98
+ }
99
+ return { strings: sorted(strings), plurals: sorted(plurals), arrays: sorted(arrays) }
100
+ }
101
+
102
+ function add<Value>(
103
+ byName: Map<string, Record<string, Value>>,
104
+ name: string,
105
+ locale: string,
106
+ value: Value,
107
+ where: string,
108
+ ) {
109
+ const values = byName.get(name) ?? {}
110
+ if (locale in values) throw new Error(`${where} is defined twice for one locale`)
111
+ values[locale] = value
112
+ byName.set(name, values)
113
+ }
114
+
115
+ // the R class code compiles against lists only the resources the default
116
+ // values define (AGP's ParseLibraryResourcesTask reads the unqualified
117
+ // directories), so a resource only a qualified directory defines has no field
118
+ const sorted = <Value>(
119
+ byName: Map<string, Record<string, Value>>,
120
+ ): AndroidResource<Value>[] =>
121
+ [...byName.entries()]
122
+ .filter(([, values]) => '' in values)
123
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
124
+ .map(([name, values]) => ({ name, values }))
125
+
126
+ // aapt2's StringBuilder over the decoded text: outside double quotes a run of
127
+ // whitespace is one space and the ends are trimmed, an unescaped apostrophe is
128
+ // an error; a double quote toggles quoting and is dropped; a backslash escapes
129
+ // the next character (\n, \t and \uXXXX, any other character as itself). a
130
+ // value beginning with @ or ? is a reference aapt2 resolves, which Peach does
131
+ // not yet
132
+ function compileString(raw: string, where: string): string {
133
+ const text = decodeXmlText(raw.replace(/<[^>]*>/g, ''), where)
134
+ if (/^\s*[@?]/.test(text))
135
+ throw new Error(`${where} is a resource reference, not resolved by peach kotlin yet`)
136
+ const out: { char: string; soft: boolean }[] = []
137
+ let quoted = false
138
+ for (let index = 0; index < text.length; index++) {
139
+ const char = text[index] ?? ''
140
+ if (char === '\\') {
141
+ const next = text[index + 1]
142
+ if (next === undefined) break
143
+ index++
144
+ if (next === 'n') out.push({ char: '\n', soft: false })
145
+ else if (next === 't') out.push({ char: '\t', soft: false })
146
+ else if (next === 'u') {
147
+ const hex = text.slice(index + 1, index + 5)
148
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
149
+ throw new Error(`${where} has an invalid unicode escape \\u${hex}`)
150
+ out.push({ char: String.fromCharCode(Number.parseInt(hex, 16)), soft: false })
151
+ index += 4
152
+ } else out.push({ char: next, soft: false })
153
+ } else if (char === '"') quoted = !quoted
154
+ else if (char === "'" && !quoted)
155
+ throw new Error(`${where} has an unescaped apostrophe`)
156
+ else if (!quoted && /\s/.test(char)) {
157
+ if (out.at(-1)?.soft !== true) out.push({ char: ' ', soft: true })
158
+ } else out.push({ char, soft: false })
159
+ }
160
+ while (out[0]?.soft) out.shift()
161
+ while (out.at(-1)?.soft) out.pop()
162
+ return out.map((part) => part.char).join('')
163
+ }
@@ -21,7 +21,7 @@ const ENTITIES: Record<string, string> = {
21
21
  const TAG = /<(\/?)([\w:.-]+)((?:\s+[\w:.-]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>/y
22
22
  const ATTRIBUTE = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
23
23
 
24
- function decode(value: string, file: string): string {
24
+ export function decodeXmlText(value: string, file: string): string {
25
25
  return value.replace(/&(#x[0-9a-fA-F]+|#\d+|\w+);/g, (_match, entity: string) => {
26
26
  if (entity.startsWith('#x'))
27
27
  return String.fromCodePoint(Number.parseInt(entity.slice(2), 16))
@@ -42,7 +42,7 @@ export function parseXml(text: string, file: string): XmlElement {
42
42
  const open = body.indexOf('<', index)
43
43
  const content = body.slice(index, open === -1 ? body.length : open)
44
44
  const parent = stack.at(-1)
45
- if (parent && content.trim()) parent.text += decode(content, file)
45
+ if (parent && content.trim()) parent.text += decodeXmlText(content, file)
46
46
  if (open === -1) break
47
47
  TAG.lastIndex = open
48
48
  const match = TAG.exec(body)
@@ -59,7 +59,7 @@ export function parseXml(text: string, file: string): XmlElement {
59
59
  }
60
60
  const attributes: Record<string, string> = {}
61
61
  for (const [, key, double, single] of (rawAttributes ?? '').matchAll(ATTRIBUTE))
62
- if (key) attributes[key] = decode(double ?? single ?? '', file)
62
+ if (key) attributes[key] = decodeXmlText(double ?? single ?? '', file)
63
63
  const element: XmlElement = { name, attributes, children: [], text: '' }
64
64
  stack.at(-1)?.children.push(element)
65
65
  if (!selfClosing) stack.push(element)
package/src/kotlin-app.ts CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  import fs from 'node:fs'
7
7
  import path from 'node:path'
8
+ import { readAndroidStrings, type AndroidStringResources } from './android-strings'
8
9
  import {
9
10
  frameworkColor,
10
11
  readResourceValues,
@@ -47,6 +48,8 @@ export interface KotlinAppProject {
47
48
  // how the activity's window meets the keyboard: pan over a covered focused
48
49
  // view, resize above the keyboard, or nothing
49
50
  softInputAdjust: 'pan' | 'resize' | 'nothing'
51
+ // the module's string resources, which its R class names
52
+ strings: AndroidStringResources
50
53
  }
51
54
 
52
55
  export interface AndroidPins {
@@ -501,5 +504,6 @@ export function readKotlinApp(
501
504
  predictiveBack,
502
505
  handlesUiMode,
503
506
  softInputAdjust,
507
+ strings: readAndroidStrings(moduleDir),
504
508
  }
505
509
  }
@@ -410,6 +410,7 @@ export async function kotlinAppArtifact(
410
410
  'PeachEntry.kt': entrySource([]),
411
411
  'PeachViews.kt': viewsSource([]),
412
412
  'PeachAppEntry.kt': appEntry([]),
413
+ ...appResourceSources(app),
413
414
  }
414
415
  const shims = [
415
416
  ...kotlinSources(path.join(moduleRoot, 'Sources')),
@@ -435,6 +436,103 @@ export async function kotlinAppArtifact(
435
436
  )
436
437
  }
437
438
 
439
+ // the module's R class in its namespace and the string, plurals and array
440
+ // tables Resources reads. an id is 0x7f, the type, then the entry in name
441
+ // order. a device's ids also count the module's other resource types and the
442
+ // resources its libraries merge in, which Peach does not build, so the
443
+ // numbers themselves differ
444
+ function appResourceSources(app: KotlinAppProject): Record<string, string> {
445
+ const escapes: Record<string, string> = {
446
+ '\\': '\\\\',
447
+ '"': '\\"',
448
+ $: '\\$',
449
+ '\n': '\\n',
450
+ '\r': '\\r',
451
+ '\t': '\\t',
452
+ }
453
+ const literal = (value: string) =>
454
+ `"${value.replace(
455
+ /[\\"$\u0000-\u001f]/g,
456
+ (char) => escapes[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`,
457
+ )}"`
458
+ // aapt2 writes a resource name's dots and dashes as underscores in R
459
+ const field = (name: string) => `\`${name.replace(/[.-]/g, '_')}\``
460
+ const id = (type: number, entry: number) =>
461
+ `0x7f${type.toString(16).padStart(2, '0')}${entry.toString(16).padStart(4, '0')}`
462
+ const kinds = [
463
+ {
464
+ type: 'string',
465
+ table: 'peachAppStrings',
466
+ value: 'String',
467
+ resources: app.strings.strings,
468
+ },
469
+ {
470
+ type: 'plurals',
471
+ table: 'peachAppPlurals',
472
+ value: 'Map<String, String>',
473
+ resources: app.strings.plurals,
474
+ },
475
+ {
476
+ type: 'array',
477
+ table: 'peachAppArrays',
478
+ value: 'List<String>',
479
+ resources: app.strings.arrays,
480
+ },
481
+ ].map((kind, index) => ({
482
+ ...kind,
483
+ entries: kind.resources.map((resource, entry) => ({
484
+ name: resource.name,
485
+ id: id(index + 1, entry),
486
+ values: Object.entries(resource.values).map(
487
+ ([locale, value]: [
488
+ string,
489
+ string | string[] | Partial<Record<string, string>>,
490
+ ]) =>
491
+ `${literal(locale)} to ${
492
+ typeof value === 'string'
493
+ ? literal(value)
494
+ : Array.isArray(value)
495
+ ? `listOf(${value.map(literal).join(', ')})`
496
+ : `mapOf(${Object.entries(value)
497
+ .map(
498
+ ([quantity, text]) =>
499
+ `${literal(quantity)} to ${literal(text ?? '')}`,
500
+ )
501
+ .join(', ')})`
502
+ }`,
503
+ ),
504
+ })),
505
+ }))
506
+ return {
507
+ 'R.kt': [
508
+ `package ${app.namespace}`,
509
+ '',
510
+ 'object R {',
511
+ ...kinds.flatMap((kind) => [
512
+ ` object ${kind.type} {`,
513
+ ...kind.entries.map(
514
+ (entry) => ` const val ${field(entry.name)}: Int = ${entry.id}`,
515
+ ),
516
+ ' }',
517
+ ]),
518
+ '}',
519
+ '',
520
+ ].join('\n'),
521
+ 'PeachAppResources.kt': [
522
+ 'package dev.peach.app',
523
+ '',
524
+ ...kinds.flatMap((kind) => [
525
+ `internal val ${kind.table}: Map<Int, Map<String, ${kind.value}>> = mapOf(`,
526
+ ...kind.entries.map(
527
+ (entry) => ` ${entry.id} to mapOf(${entry.values.join(', ')}),`,
528
+ ),
529
+ ')',
530
+ '',
531
+ ]),
532
+ ].join('\n'),
533
+ }
534
+ }
535
+
438
536
  async function compileKotlin(
439
537
  input: {
440
538
  root: string