peachsim 0.1.648 → 0.1.650
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/README.md +10 -9
- package/cli/command-registry.ts +2 -1
- package/cli/commands/guest-host.ts +67 -0
- package/cli/commands/kotlin.ts +231 -0
- package/cli/commands/project-report.ts +1 -1
- package/cli/commands/report-issue.ts +55 -32
- package/cli/commands/setup.ts +10 -14
- package/cli/commands/support.ts +399 -0
- package/cli/commands/swift.ts +6 -52
- package/cli/commands/telemetry.ts +10 -13
- package/cli/main.ts +22 -10
- package/cli/outbound-endpoints.ts +23 -2
- package/cli/privacy.ts +23 -25
- package/cli/runtime-notes.ts +3 -2
- package/cli/telemetry.ts +109 -14
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +3 -3
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +102 -21
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +28 -39
- package/dist-lib/swift-compile.cjs +6 -3
- package/dist-lib/swift-compile.mjs +6 -3
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +6 -3
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/app/Activity.kt +85 -0
- package/kotlin/AppSources/android/app/Application.kt +7 -0
- package/kotlin/AppSources/android/content/Context.kt +5 -0
- package/kotlin/AppSources/android/content/Intent.kt +16 -0
- package/kotlin/AppSources/android/os/Bundle.kt +5 -0
- package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
- package/kotlin/AppSources/android/view/View.kt +72 -0
- package/kotlin/AppSources/android/view/Window.kt +44 -0
- package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
- package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
- package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +316 -0
- package/kotlin/Sources/dev/onejs/one/OneNativeSourceViewDispatch.kt +13 -0
- package/kotlin/Sources/org/json/JSON.kt +6 -0
- package/kotlin/fixtures/counter/Counter.d.kt.ts +16 -0
- package/kotlin/fixtures/counter/Counter.kt +18 -0
- package/kotlin/fixtures/counter/CounterApp.native.tsx +34 -0
- package/kotlin/fixtures/counter/CounterApp.tsx +3 -0
- package/kotlin/fixtures/counter/CounterScreen.d.kt.ts +7 -0
- package/kotlin/fixtures/counter/CounterScreen.kt +18 -3
- package/kotlin/fixtures/counter/app/_layout.tsx +4 -0
- package/kotlin/fixtures/counter/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/counter/app/routes.d.ts +18 -0
- package/kotlin/fixtures/counter/package.json +7 -0
- package/kotlin/fixtures/counter/tsconfig.json +19 -0
- package/kotlin/fixtures/counter/vite.config.ts +10 -0
- package/kotlin/fixtures/headless-counter/Headless.kt +5 -0
- package/kotlin/fixtures/kotlin-app/app/build.gradle.kts +30 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/AndroidManifest.xml +13 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/MainActivity.kt +60 -0
- package/kotlin/fixtures/kotlin-app/app/src/main/java/dev/contrast/kotlinapp/ProbeApplication.kt +18 -0
- package/kotlin/fixtures/kotlin-app/build.gradle.kts +5 -0
- package/kotlin/fixtures/kotlin-app/gradle.properties +4 -0
- package/kotlin/fixtures/kotlin-app/settings.gradle.kts +4 -0
- package/kotlin/fork/NOTICE +7 -5
- package/kotlin/fork/README.md +41 -49
- package/kotlin/fork/build.sh +3 -2
- package/kotlin/fork/compose-worker-globals.patch +22 -11
- package/kotlin/fork/fork.json +2 -2
- package/kotlin/fork/publish.ts +30 -15
- package/kotlin/fork/skiko-worker-clock.patch +38 -0
- package/kotlin/toolchain.json +9 -9
- package/package.json +1 -1
- package/skills/contrast/SKILL.md +2 -1
- package/skills/peach-setup/SKILL.md +2 -2
- package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
- package/src/home-paths.ts +5 -3
- package/src/kotlin-app.ts +411 -0
- package/src/kotlin-host.ts +148 -25
- package/src/swift-compile.ts +7 -2
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// reads a pure Kotlin Android app module the way its Gradle build and the
|
|
2
|
+
// device resolve it, for the subset Peach boots: one module's `main` source
|
|
3
|
+
// set, its own manifest, literal Gradle values and the pinned Compose closure.
|
|
4
|
+
// anything outside that subset rejects with its location, never approximated.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
|
|
9
|
+
export interface KotlinAppProject {
|
|
10
|
+
projectDir: string
|
|
11
|
+
moduleDir: string
|
|
12
|
+
sources: string[]
|
|
13
|
+
language: string
|
|
14
|
+
packageName: string
|
|
15
|
+
namespace: string
|
|
16
|
+
targetSdk: number
|
|
17
|
+
optOutEdgeToEdge: boolean
|
|
18
|
+
applicationClass: string | null
|
|
19
|
+
launcher: { name: string; targetClass: string }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ComposeAndroidPins {
|
|
23
|
+
ui: string
|
|
24
|
+
foundation: string
|
|
25
|
+
runtime: string
|
|
26
|
+
material3: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ANDROID_NS = 'android:'
|
|
30
|
+
const MAIN = 'android.intent.action.MAIN'
|
|
31
|
+
const LAUNCHER = 'android.intent.category.LAUNCHER'
|
|
32
|
+
const PLUGINS = new Set([
|
|
33
|
+
'com.android.application',
|
|
34
|
+
'org.jetbrains.kotlin.android',
|
|
35
|
+
'org.jetbrains.kotlin.plugin.compose',
|
|
36
|
+
])
|
|
37
|
+
// the closure Peach links. compose artifacts must match its Android pins; the
|
|
38
|
+
// activity, lifecycle and savedstate artifacts are Peach's platform actuals.
|
|
39
|
+
const PLATFORM_GROUPS = new Set([
|
|
40
|
+
'androidx.activity',
|
|
41
|
+
'androidx.lifecycle',
|
|
42
|
+
'androidx.savedstate',
|
|
43
|
+
])
|
|
44
|
+
const COMPOSE_PIN: Record<string, keyof ComposeAndroidPins> = {
|
|
45
|
+
'androidx.compose.ui': 'ui',
|
|
46
|
+
'androidx.compose.foundation': 'foundation',
|
|
47
|
+
'androidx.compose.runtime': 'runtime',
|
|
48
|
+
'androidx.compose.material3': 'material3',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface XmlElement {
|
|
52
|
+
name: string
|
|
53
|
+
attributes: Record<string, string>
|
|
54
|
+
children: XmlElement[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// manifest XML: elements and quoted attributes; comments, declarations and
|
|
58
|
+
// text are skipped
|
|
59
|
+
function parseXml(text: string, file: string): XmlElement {
|
|
60
|
+
const root: XmlElement = { name: '#document', attributes: {}, children: [] }
|
|
61
|
+
const stack = [root]
|
|
62
|
+
const body = text.replace(/<!--[\s\S]*?-->/g, '').replace(/<\?[\s\S]*?\?>/g, '')
|
|
63
|
+
for (const match of body.matchAll(
|
|
64
|
+
/<(\/?)([\w:.-]+)((?:\s+[\w:.-]+\s*=\s*"[^"]*")*)\s*(\/?)>/g,
|
|
65
|
+
)) {
|
|
66
|
+
const [, closing, name, rawAttributes, selfClosing] = match
|
|
67
|
+
if (!name) continue
|
|
68
|
+
if (closing) {
|
|
69
|
+
const open = stack.pop()
|
|
70
|
+
if (open?.name !== name)
|
|
71
|
+
throw new Error(`${file}: </${name}> closes <${open?.name}>`)
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
const attributes: Record<string, string> = {}
|
|
75
|
+
for (const [, key, value] of (rawAttributes ?? '').matchAll(
|
|
76
|
+
/([\w:.-]+)\s*=\s*"([^"]*)"/g,
|
|
77
|
+
))
|
|
78
|
+
if (key)
|
|
79
|
+
attributes[key] = (value ?? '').replaceAll('"', '"').replaceAll('&', '&')
|
|
80
|
+
const element: XmlElement = { name, attributes, children: [] }
|
|
81
|
+
stack.at(-1)?.children.push(element)
|
|
82
|
+
if (!selfClosing) stack.push(element)
|
|
83
|
+
}
|
|
84
|
+
if (stack.length !== 1)
|
|
85
|
+
throw new Error(`${file}: <${stack.at(-1)?.name}> is never closed`)
|
|
86
|
+
return root
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readGradle(dir: string): { file: string; text: string } | null {
|
|
90
|
+
for (const name of ['build.gradle.kts', 'build.gradle']) {
|
|
91
|
+
const file = path.join(dir, name)
|
|
92
|
+
if (fs.existsSync(file)) return { file, text: fs.readFileSync(file, 'utf8') }
|
|
93
|
+
}
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function lineOf(text: string, index: number): number {
|
|
98
|
+
return text.slice(0, index).split('\n').length
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// a literal `name = value` or `name value`; any other assignment of the name
|
|
102
|
+
// is a value Peach cannot read without running Gradle
|
|
103
|
+
function gradleLiteral(
|
|
104
|
+
gradle: { file: string; text: string },
|
|
105
|
+
name: string,
|
|
106
|
+
pattern: string,
|
|
107
|
+
): string | null {
|
|
108
|
+
const literal = new RegExp(`\\b${name}\\s*=?\\s*(${pattern})\\s*$`, 'm').exec(
|
|
109
|
+
gradle.text,
|
|
110
|
+
)
|
|
111
|
+
if (literal?.[1]) return literal[1].replace(/^"|"$/g, '')
|
|
112
|
+
const any = new RegExp(`\\b${name}\\b\\s*=?\\s*\\S`, 'm').exec(gradle.text)
|
|
113
|
+
if (any)
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${gradle.file}:${lineOf(gradle.text, any.index)}: ${name} must be a literal for peach kotlin`,
|
|
116
|
+
)
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function kotlinPluginVersion(
|
|
121
|
+
projectDir: string,
|
|
122
|
+
moduleGradle: { file: string; text: string },
|
|
123
|
+
) {
|
|
124
|
+
const sources = [readGradle(projectDir), moduleGradle].filter((value) => value !== null)
|
|
125
|
+
for (const gradle of sources) {
|
|
126
|
+
const match =
|
|
127
|
+
/id\(\s*"org\.jetbrains\.kotlin\.android"\s*\)\s*version\s*"([^"]+)"/.exec(
|
|
128
|
+
gradle.text,
|
|
129
|
+
) ?? /kotlin\(\s*"android"\s*\)\s*version\s*"([^"]+)"/.exec(gradle.text)
|
|
130
|
+
if (match?.[1]) return match[1]
|
|
131
|
+
}
|
|
132
|
+
const catalog = path.join(projectDir, 'gradle', 'libs.versions.toml')
|
|
133
|
+
if (fs.existsSync(catalog)) {
|
|
134
|
+
const match = /^kotlin\s*=\s*"([^"]+)"/m.exec(fs.readFileSync(catalog, 'utf8'))
|
|
135
|
+
if (match?.[1]) return match[1]
|
|
136
|
+
}
|
|
137
|
+
throw new Error(
|
|
138
|
+
`${projectDir} declares no literal org.jetbrains.kotlin.android plugin version`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function checkModuleGradle(
|
|
143
|
+
gradle: { file: string; text: string },
|
|
144
|
+
pins: ComposeAndroidPins,
|
|
145
|
+
) {
|
|
146
|
+
const at = (index: number) => `${gradle.file}:${lineOf(gradle.text, index)}`
|
|
147
|
+
for (const match of gradle.text.matchAll(
|
|
148
|
+
/\bid\(\s*"([^"]+)"\s*\)|\bkotlin\(\s*"([^"]+)"\s*\)/g,
|
|
149
|
+
)) {
|
|
150
|
+
const plugin = match[1] ?? `org.jetbrains.kotlin.${match[2]}`
|
|
151
|
+
if (!PLUGINS.has(plugin))
|
|
152
|
+
throw new Error(
|
|
153
|
+
`${at(match.index)}: plugin ${plugin} is not supported by peach kotlin`,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
for (const block of gradle.text.matchAll(/\bdependencies\s*\{/g)) {
|
|
157
|
+
// the block runs to its matching brace, on one line or many; every
|
|
158
|
+
// statement in it must be a literal dependency Peach can check
|
|
159
|
+
const start = block.index + block[0].length
|
|
160
|
+
let depth = 1
|
|
161
|
+
let end = start
|
|
162
|
+
for (; end < gradle.text.length && depth > 0; end++) {
|
|
163
|
+
if (gradle.text[end] === '{') depth++
|
|
164
|
+
else if (gradle.text[end] === '}') depth--
|
|
165
|
+
}
|
|
166
|
+
if (depth > 0) throw new Error(`${at(block.index)}: dependencies block never closes`)
|
|
167
|
+
const body = gradle.text.slice(start, end - 1)
|
|
168
|
+
for (const statement of body.matchAll(/[^\n;]+/g)) {
|
|
169
|
+
const text = statement[0].replace(/\/\/.*$/, '').trim()
|
|
170
|
+
if (!text) continue
|
|
171
|
+
const where = at(start + statement.index)
|
|
172
|
+
const call = /^(\w+)\s*\(\s*(.*?)\s*\)$/.exec(text)
|
|
173
|
+
if (!call) throw new Error(`${where}: ${text} is not a literal dependency`)
|
|
174
|
+
const [, configuration = '', value = ''] = call
|
|
175
|
+
if (/^(test|androidTest|debug|release)/.test(configuration)) continue
|
|
176
|
+
const coordinate = /^["']([^"':]+):([^"':]+):([^"']+)["']$/.exec(value)
|
|
177
|
+
if (!coordinate)
|
|
178
|
+
throw new Error(`${where}: ${value} is not a literal group:artifact:version`)
|
|
179
|
+
const [, group = '', artifact, version] = coordinate
|
|
180
|
+
const pin = COMPOSE_PIN[group]
|
|
181
|
+
if (pin) {
|
|
182
|
+
if (version !== pins[pin])
|
|
183
|
+
throw new Error(
|
|
184
|
+
`${where}: ${group}:${artifact}:${version} differs from Peach's Compose ${pin} ${pins[pin]}`,
|
|
185
|
+
)
|
|
186
|
+
} else if (!PLATFORM_GROUPS.has(group))
|
|
187
|
+
throw new Error(`${where}: ${group}:${artifact} is not in Peach's Kotlin closure`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function themeOptsOut(moduleDir: string, theme: string | undefined): boolean {
|
|
193
|
+
const name = theme?.startsWith('@style/') ? theme.slice('@style/'.length) : null
|
|
194
|
+
if (!name) return false
|
|
195
|
+
const values = path.join(moduleDir, 'src/main/res/values')
|
|
196
|
+
if (!fs.existsSync(values)) throw new Error(`${theme} names no style in ${values}`)
|
|
197
|
+
for (const file of fs.readdirSync(values).filter((entry) => entry.endsWith('.xml'))) {
|
|
198
|
+
const full = path.join(values, file)
|
|
199
|
+
const resources = parseXml(fs.readFileSync(full, 'utf8'), full)
|
|
200
|
+
for (const style of resources.children.flatMap((element) => element.children)) {
|
|
201
|
+
if (style.name !== 'style' || style.attributes.name !== name) continue
|
|
202
|
+
if (style.attributes.parent?.startsWith('@style/'))
|
|
203
|
+
throw new Error(
|
|
204
|
+
`${full}: style ${name} inherits an app style; peach kotlin reads one level`,
|
|
205
|
+
)
|
|
206
|
+
const items = fs
|
|
207
|
+
.readFileSync(full, 'utf8')
|
|
208
|
+
.match(new RegExp(`<style\\b[^>]*name="${name}"[\\s\\S]*?</style>`))?.[0]
|
|
209
|
+
return /<item\s+name="android:windowOptOutEdgeToEdgeEnforcement"\s*>\s*true\s*</.test(
|
|
210
|
+
items ?? '',
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`${theme} names no style in ${values}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function enabled(element: XmlElement): boolean {
|
|
218
|
+
return element.attributes[`${ANDROID_NS}enabled`] !== 'false'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// PackageParser: a component with an intent filter must declare
|
|
222
|
+
// android:exported from target SDK 31 and defaults to exported below it.
|
|
223
|
+
// launching from the home screen or the shell needs an exported component.
|
|
224
|
+
function exported(element: XmlElement, targetSdk: number, manifestFile: string): boolean {
|
|
225
|
+
const value = element.attributes[`${ANDROID_NS}exported`]
|
|
226
|
+
if (value !== undefined) return value === 'true'
|
|
227
|
+
const filtered = element.children.some((child) => child.name === 'intent-filter')
|
|
228
|
+
if (filtered && targetSdk >= 31)
|
|
229
|
+
throw new Error(
|
|
230
|
+
`${manifestFile}: ${element.attributes[`${ANDROID_NS}name`]} has an intent filter and no android:exported, which fails to install from target SDK 31`,
|
|
231
|
+
)
|
|
232
|
+
return filtered
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isLauncher(element: XmlElement): boolean {
|
|
236
|
+
return element.children.some(
|
|
237
|
+
(filter) =>
|
|
238
|
+
filter.name === 'intent-filter' &&
|
|
239
|
+
filter.children.some(
|
|
240
|
+
(child) =>
|
|
241
|
+
child.name === 'action' && child.attributes[`${ANDROID_NS}name`] === MAIN,
|
|
242
|
+
) &&
|
|
243
|
+
filter.children.some(
|
|
244
|
+
(child) =>
|
|
245
|
+
child.name === 'category' && child.attributes[`${ANDROID_NS}name`] === LAUNCHER,
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function readKotlinApp(
|
|
251
|
+
moduleArg: string,
|
|
252
|
+
options: { activity?: string; pins: ComposeAndroidPins },
|
|
253
|
+
): KotlinAppProject {
|
|
254
|
+
const moduleDir = fs.realpathSync(path.resolve(moduleArg))
|
|
255
|
+
const manifestFile = path.join(moduleDir, 'src/main/AndroidManifest.xml')
|
|
256
|
+
if (!fs.existsSync(manifestFile))
|
|
257
|
+
throw new Error(
|
|
258
|
+
`${moduleDir} is not an Android app module: no src/main/AndroidManifest.xml`,
|
|
259
|
+
)
|
|
260
|
+
let projectDir = moduleDir
|
|
261
|
+
while (
|
|
262
|
+
!['settings.gradle.kts', 'settings.gradle'].some((name) =>
|
|
263
|
+
fs.existsSync(path.join(projectDir, name)),
|
|
264
|
+
)
|
|
265
|
+
) {
|
|
266
|
+
const parent = path.dirname(projectDir)
|
|
267
|
+
if (parent === projectDir)
|
|
268
|
+
throw new Error(`${moduleDir} has no settings.gradle above it`)
|
|
269
|
+
projectDir = parent
|
|
270
|
+
}
|
|
271
|
+
const gradle = readGradle(moduleDir)
|
|
272
|
+
if (!gradle) throw new Error(`${moduleDir} has no build.gradle`)
|
|
273
|
+
checkModuleGradle(gradle, options.pins)
|
|
274
|
+
const namespace = gradleLiteral(gradle, 'namespace', '"[\\w.]+"')
|
|
275
|
+
const applicationId = gradleLiteral(gradle, 'applicationId', '"[\\w.]+"')
|
|
276
|
+
const targetSdk = Number(gradleLiteral(gradle, 'targetSdk(?:Version)?', '\\d+'))
|
|
277
|
+
if (!Number.isInteger(targetSdk) || targetSdk <= 0)
|
|
278
|
+
throw new Error(`${gradle.file}: declares no literal targetSdk`)
|
|
279
|
+
const kotlin = kotlinPluginVersion(projectDir, gradle)
|
|
280
|
+
const language = /^(\d+)\.(\d+)/.exec(kotlin)?.slice(1, 3).join('.')
|
|
281
|
+
if (!language)
|
|
282
|
+
throw new Error(`Kotlin plugin version ${kotlin} names no language version`)
|
|
283
|
+
|
|
284
|
+
const manifest = parseXml(fs.readFileSync(manifestFile, 'utf8'), manifestFile)
|
|
285
|
+
const root = manifest.children.find((element) => element.name === 'manifest')
|
|
286
|
+
const application = root?.children.find((element) => element.name === 'application')
|
|
287
|
+
if (!root || !application) throw new Error(`${manifestFile} has no <application>`)
|
|
288
|
+
const base = namespace ?? root.attributes.package
|
|
289
|
+
if (!base)
|
|
290
|
+
throw new Error(`${gradle.file}: declares no namespace and the manifest no package`)
|
|
291
|
+
const className = (name: string) =>
|
|
292
|
+
name.startsWith('.') ? base + name : name.includes('.') ? name : `${base}.${name}`
|
|
293
|
+
if (application.attributes[`${ANDROID_NS}appComponentFactory`])
|
|
294
|
+
throw new Error(
|
|
295
|
+
`${manifestFile}: android:appComponentFactory is not supported by peach kotlin`,
|
|
296
|
+
)
|
|
297
|
+
for (const element of application.children) {
|
|
298
|
+
if (element.name === 'provider')
|
|
299
|
+
throw new Error(
|
|
300
|
+
`${manifestFile}: <provider ${element.attributes[`${ANDROID_NS}name`]}> is not supported by peach kotlin`,
|
|
301
|
+
)
|
|
302
|
+
if (element.attributes[`${ANDROID_NS}process`])
|
|
303
|
+
throw new Error(
|
|
304
|
+
`${manifestFile}: android:process on ${element.attributes[`${ANDROID_NS}name`]} is not supported by peach kotlin`,
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
if (application.attributes[`${ANDROID_NS}process`])
|
|
308
|
+
throw new Error(
|
|
309
|
+
`${manifestFile}: android:process on <application> is not supported by peach kotlin`,
|
|
310
|
+
)
|
|
311
|
+
for (const [key, value] of [
|
|
312
|
+
...[root, application].flatMap((element) => Object.entries(element.attributes)),
|
|
313
|
+
])
|
|
314
|
+
if (value.includes('${'))
|
|
315
|
+
throw new Error(`${manifestFile}: ${key}="${value}" is a Gradle placeholder`)
|
|
316
|
+
|
|
317
|
+
const activities = application.children.filter((element) => element.name === 'activity')
|
|
318
|
+
const components = application.children.filter(
|
|
319
|
+
(element) => element.name === 'activity' || element.name === 'activity-alias',
|
|
320
|
+
)
|
|
321
|
+
const target = (element: XmlElement) => {
|
|
322
|
+
const name = element.attributes[`${ANDROID_NS}name`]
|
|
323
|
+
if (!name) throw new Error(`${manifestFile}: <${element.name}> has no android:name`)
|
|
324
|
+
if (element.name === 'activity') return className(name)
|
|
325
|
+
const targetName = element.attributes[`${ANDROID_NS}targetActivity`]
|
|
326
|
+
const activity = activities.find(
|
|
327
|
+
(candidate) =>
|
|
328
|
+
className(candidate.attributes[`${ANDROID_NS}name`] ?? '') ===
|
|
329
|
+
className(targetName ?? ''),
|
|
330
|
+
)
|
|
331
|
+
if (!targetName || !activity)
|
|
332
|
+
throw new Error(
|
|
333
|
+
`${manifestFile}: activity-alias ${name} targets no declared activity`,
|
|
334
|
+
)
|
|
335
|
+
if (!enabled(activity)) return null
|
|
336
|
+
return className(targetName)
|
|
337
|
+
}
|
|
338
|
+
const applicationEnabled = enabled(application)
|
|
339
|
+
let chosen: XmlElement | undefined
|
|
340
|
+
if (options.activity) {
|
|
341
|
+
const wanted = className(options.activity)
|
|
342
|
+
chosen = components.find(
|
|
343
|
+
(element) => className(element.attributes[`${ANDROID_NS}name`] ?? '') === wanted,
|
|
344
|
+
)
|
|
345
|
+
if (!chosen)
|
|
346
|
+
throw new Error(`Error type 3: Activity class {${base}/${wanted}} does not exist.`)
|
|
347
|
+
if (!applicationEnabled || !enabled(chosen) || target(chosen) === null)
|
|
348
|
+
throw new Error(`Activity {${base}/${wanted}} is disabled`)
|
|
349
|
+
if (!exported(chosen, targetSdk, manifestFile))
|
|
350
|
+
throw new Error(`Permission Denial: ${base}/${wanted} is not exported`)
|
|
351
|
+
} else {
|
|
352
|
+
const launchers = applicationEnabled
|
|
353
|
+
? components.filter(
|
|
354
|
+
(element) =>
|
|
355
|
+
enabled(element) &&
|
|
356
|
+
isLauncher(element) &&
|
|
357
|
+
exported(element, targetSdk, manifestFile) &&
|
|
358
|
+
target(element) !== null,
|
|
359
|
+
)
|
|
360
|
+
: []
|
|
361
|
+
if (launchers.length === 0)
|
|
362
|
+
throw new Error(
|
|
363
|
+
`${manifestFile}: no enabled activity resolves the MAIN/LAUNCHER intent`,
|
|
364
|
+
)
|
|
365
|
+
if (launchers.length > 1)
|
|
366
|
+
throw new Error(
|
|
367
|
+
`${manifestFile}: ${launchers.map((element) => element.attributes[`${ANDROID_NS}name`]).join(', ')} all resolve the launcher intent; pick one with --activity`,
|
|
368
|
+
)
|
|
369
|
+
chosen = launchers[0]
|
|
370
|
+
}
|
|
371
|
+
const launcherName = className(chosen?.attributes[`${ANDROID_NS}name`] ?? '')
|
|
372
|
+
const targetClass = chosen ? target(chosen) : null
|
|
373
|
+
if (!targetClass) throw new Error(`${launcherName} targets a disabled activity`)
|
|
374
|
+
const themeOwner = activities.find(
|
|
375
|
+
(element) => className(element.attributes[`${ANDROID_NS}name`] ?? '') === targetClass,
|
|
376
|
+
)
|
|
377
|
+
const theme =
|
|
378
|
+
themeOwner?.attributes[`${ANDROID_NS}theme`] ??
|
|
379
|
+
application.attributes[`${ANDROID_NS}theme`]
|
|
380
|
+
const applicationName = application.attributes[`${ANDROID_NS}name`]
|
|
381
|
+
|
|
382
|
+
const sources: string[] = []
|
|
383
|
+
for (const set of ['java', 'kotlin']) {
|
|
384
|
+
const dir = path.join(moduleDir, 'src/main', set)
|
|
385
|
+
if (!fs.existsSync(dir)) continue
|
|
386
|
+
const walk = (current: string) => {
|
|
387
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
388
|
+
const full = path.join(current, entry.name)
|
|
389
|
+
if (entry.isDirectory()) walk(full)
|
|
390
|
+
else if (entry.name.endsWith('.java'))
|
|
391
|
+
throw new Error(`${full}: Java sources are not supported by peach kotlin`)
|
|
392
|
+
else if (entry.name.endsWith('.kt')) sources.push(full)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
walk(dir)
|
|
396
|
+
}
|
|
397
|
+
if (sources.length === 0)
|
|
398
|
+
throw new Error(`${moduleDir}/src/main holds no Kotlin sources`)
|
|
399
|
+
return {
|
|
400
|
+
projectDir,
|
|
401
|
+
moduleDir,
|
|
402
|
+
sources: sources.sort(),
|
|
403
|
+
language,
|
|
404
|
+
packageName: applicationId ?? base,
|
|
405
|
+
namespace: base,
|
|
406
|
+
targetSdk,
|
|
407
|
+
optOutEdgeToEdge: themeOptsOut(moduleDir, theme),
|
|
408
|
+
applicationClass: applicationName ? className(applicationName) : null,
|
|
409
|
+
launcher: { name: launcherName, targetClass },
|
|
410
|
+
}
|
|
411
|
+
}
|
package/src/kotlin-host.ts
CHANGED
|
@@ -12,6 +12,7 @@ import path from 'node:path'
|
|
|
12
12
|
import { promisify } from 'node:util'
|
|
13
13
|
import { rnxHomeDir } from './home-paths'
|
|
14
14
|
import { isRecord } from './is-record'
|
|
15
|
+
import type { KotlinAppProject } from './kotlin-app'
|
|
15
16
|
import type { ServerResponse } from 'node:http'
|
|
16
17
|
|
|
17
18
|
const run = promisify(execFile)
|
|
@@ -192,11 +193,16 @@ const SKIP_DIRS = new Set([
|
|
|
192
193
|
'scripts',
|
|
193
194
|
])
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
// peach's own platform sources live in package dirs such as `android`, so
|
|
197
|
+
// they walk with no skip set
|
|
198
|
+
export function kotlinSources(
|
|
199
|
+
root: string,
|
|
200
|
+
skip: ReadonlySet<string> = SKIP_DIRS,
|
|
201
|
+
): string[] {
|
|
196
202
|
const sources: string[] = []
|
|
197
203
|
const walk = (dir: string) => {
|
|
198
204
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
199
|
-
if (entry.name.startsWith('.') ||
|
|
205
|
+
if (entry.name.startsWith('.') || skip.has(entry.name)) continue
|
|
200
206
|
const full = path.join(dir, entry.name)
|
|
201
207
|
if (entry.isDirectory()) walk(full)
|
|
202
208
|
else if (entry.isFile() && entry.name.endsWith('.kt')) sources.push(full)
|
|
@@ -256,7 +262,7 @@ function entrySource(ids: string[]): string {
|
|
|
256
262
|
].join('\n')
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
const BUILD_FORMAT =
|
|
265
|
+
const BUILD_FORMAT = 7
|
|
260
266
|
const builds = new Map<string, Promise<KotlinArtifact>>()
|
|
261
267
|
|
|
262
268
|
function buildsDir(): string {
|
|
@@ -278,18 +284,105 @@ export async function kotlinArtifact(
|
|
|
278
284
|
if (sources.length === 0) throw new Error(`${root} holds no Kotlin sources`)
|
|
279
285
|
const generated: Record<string, string> = {}
|
|
280
286
|
const ids: string[] = []
|
|
287
|
+
const viewIds: string[] = []
|
|
281
288
|
let usesCompose = false
|
|
282
289
|
for (const file of sources) {
|
|
283
290
|
const source = fs.readFileSync(file, 'utf8')
|
|
284
291
|
if (/\bandroidx\.compose\./.test(source)) usesCompose = true
|
|
285
292
|
const contract = nativeSourceContract(file, source)
|
|
286
|
-
if (contract.modules.length === 0) continue
|
|
293
|
+
if (contract.modules.length === 0 && contract.views.length === 0) continue
|
|
287
294
|
const id = kotlinSourceId(root, file)
|
|
288
|
-
ids.push(id)
|
|
295
|
+
if (contract.modules.length > 0) ids.push(id)
|
|
296
|
+
if (contract.views.length > 0) viewIds.push(id)
|
|
289
297
|
generated[`OneNativeSource_${id}.kt`] = renderKotlinSourceGlue(id, contract).source
|
|
290
298
|
}
|
|
291
299
|
generated['PeachEntry.kt'] = entrySource(ids)
|
|
292
|
-
|
|
300
|
+
if (viewIds.length > 0) generated['PeachViews.kt'] = viewsSource(viewIds)
|
|
301
|
+
const shims = kotlinSources(path.join(moduleRoot, 'Sources')).filter(
|
|
302
|
+
(file) => usesCompose || !file.endsWith('/OneNativeSourceViewDispatch.kt'),
|
|
303
|
+
)
|
|
304
|
+
if (viewIds.length > 0)
|
|
305
|
+
shims.push(...kotlinSources(path.join(moduleRoot, 'ComposeSources')))
|
|
306
|
+
return compileKotlin(
|
|
307
|
+
{ root, toolchain, language, sources, generated, shims, usesCompose },
|
|
308
|
+
origin,
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function viewsSource(viewIds: string[]): string {
|
|
313
|
+
return [
|
|
314
|
+
'import dev.onejs.one.OneNativeSourceViewDispatch',
|
|
315
|
+
'',
|
|
316
|
+
'fun peachViewDispatcher(source: String): OneNativeSourceViewDispatch = when (source) {',
|
|
317
|
+
...viewIds.map((id) => ` ${JSON.stringify(id)} -> OneNativeSourceViews_${id}()`),
|
|
318
|
+
' else -> error("composable source $source is not linked; rebuild the app")',
|
|
319
|
+
'}',
|
|
320
|
+
].join('\n')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// a pure Kotlin app: its module's sources, the Android platform actuals and
|
|
324
|
+
// the compose host, with an entry that cold-starts the resolved launcher
|
|
325
|
+
export async function kotlinAppArtifact(
|
|
326
|
+
app: KotlinAppProject,
|
|
327
|
+
moduleRoot: string,
|
|
328
|
+
origin: string,
|
|
329
|
+
): Promise<KotlinArtifact> {
|
|
330
|
+
const toolchain = readToolchain(moduleRoot)
|
|
331
|
+
const kotlinString = (value: string) => JSON.stringify(value).replaceAll('$', '\\$')
|
|
332
|
+
const generated: Record<string, string> = {
|
|
333
|
+
'PeachEntry.kt': entrySource([]),
|
|
334
|
+
'PeachViews.kt': viewsSource([]),
|
|
335
|
+
'PeachAppEntry.kt': [
|
|
336
|
+
'@file:OptIn(kotlin.js.ExperimentalJsExport::class)',
|
|
337
|
+
'',
|
|
338
|
+
'@JsExport',
|
|
339
|
+
'fun peachAppMount(density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,',
|
|
340
|
+
' regular: String, medium: String, bold: String, statusBar: Int, navigationBar: Int,',
|
|
341
|
+
' invalidate: (Boolean) -> Unit): String = dev.peach.app.peachStartApp(',
|
|
342
|
+
' dev.peach.app.PeachAppSpec(',
|
|
343
|
+
` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.name)}),`,
|
|
344
|
+
` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.targetClass)}),`,
|
|
345
|
+
` ${app.targetSdk}, ${app.optOutEdgeToEdge},`,
|
|
346
|
+
` { ${app.applicationClass ?? 'android.app.Application'}() },`,
|
|
347
|
+
` { ${app.launcher.targetClass}() },`,
|
|
348
|
+
' ),',
|
|
349
|
+
' density, fontScale, locale, backend, stencilBits, regular, medium, bold, statusBar, navigationBar, invalidate,',
|
|
350
|
+
')',
|
|
351
|
+
'',
|
|
352
|
+
].join('\n'),
|
|
353
|
+
}
|
|
354
|
+
const shims = [
|
|
355
|
+
...kotlinSources(path.join(moduleRoot, 'Sources')),
|
|
356
|
+
...kotlinSources(path.join(moduleRoot, 'ComposeSources')),
|
|
357
|
+
...kotlinSources(path.join(moduleRoot, 'AppSources'), new Set()),
|
|
358
|
+
]
|
|
359
|
+
return compileKotlin(
|
|
360
|
+
{
|
|
361
|
+
root: app.projectDir,
|
|
362
|
+
toolchain,
|
|
363
|
+
language: app.language,
|
|
364
|
+
sources: app.sources,
|
|
365
|
+
generated,
|
|
366
|
+
shims,
|
|
367
|
+
usesCompose: true,
|
|
368
|
+
},
|
|
369
|
+
origin,
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function compileKotlin(
|
|
374
|
+
input: {
|
|
375
|
+
root: string
|
|
376
|
+
toolchain: KotlinToolchain
|
|
377
|
+
language: string
|
|
378
|
+
sources: string[]
|
|
379
|
+
generated: Record<string, string>
|
|
380
|
+
shims: string[]
|
|
381
|
+
usesCompose: boolean
|
|
382
|
+
},
|
|
383
|
+
origin: string,
|
|
384
|
+
): Promise<KotlinArtifact> {
|
|
385
|
+
const { root, toolchain, language, sources, generated, shims, usesCompose } = input
|
|
293
386
|
const digest = createHash('sha256')
|
|
294
387
|
// BUILD_FORMAT moves when this host changes what it does to a build
|
|
295
388
|
digest.update(JSON.stringify({ format: BUILD_FORMAT, toolchain, language }))
|
|
@@ -424,25 +517,46 @@ export async function kotlinArtifact(
|
|
|
424
517
|
`kotlin compile failed:\n${output.trim() || (error instanceof Error ? error.message : String(error))}`,
|
|
425
518
|
)
|
|
426
519
|
}
|
|
427
|
-
// Kotlin
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
520
|
+
// the Kotlin module shares the tenant's react native globals, which a
|
|
521
|
+
// Kotlin process never sees. Kotlin's loader picks node when
|
|
522
|
+
// `process.release.name` says so, and react native's `process` has no
|
|
523
|
+
// release, so the read becomes optional. kotlinx-coroutines dispatches
|
|
524
|
+
// through `window.postMessage(msg, '*')` when it finds a window with
|
|
525
|
+
// `addEventListener`, which the tenant's window has and a worker cannot
|
|
526
|
+
// honor, so the module takes the setTimeout dispatcher a worker gets.
|
|
527
|
+
const seams: Array<[string, string]> = [
|
|
528
|
+
['process.release.name', 'process.release?.name'],
|
|
529
|
+
]
|
|
530
|
+
const js = fs
|
|
531
|
+
.readdirSync(path.join(work, 'js'))
|
|
532
|
+
.filter((file) => file.endsWith('.mjs'))
|
|
533
|
+
if (
|
|
534
|
+
js.some((file) =>
|
|
535
|
+
fs
|
|
536
|
+
.readFileSync(path.join(work, 'js', file), 'utf8')
|
|
537
|
+
.includes("'kotlinx.coroutines."),
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
seams.push([
|
|
541
|
+
"(typeof(window) !== 'undefined' && window != null && typeof(window.addEventListener) === 'function') ? window : null",
|
|
542
|
+
'null',
|
|
543
|
+
])
|
|
544
|
+
for (const [from, to] of seams) {
|
|
545
|
+
let patched = 0
|
|
546
|
+
for (const file of js) {
|
|
547
|
+
const target = path.join(work, 'js', file)
|
|
548
|
+
const text = fs.readFileSync(target, 'utf8')
|
|
549
|
+
const next = text.replaceAll(from, to)
|
|
550
|
+
if (next !== text) {
|
|
551
|
+
patched++
|
|
552
|
+
fs.writeFileSync(target, next)
|
|
553
|
+
}
|
|
440
554
|
}
|
|
555
|
+
if (patched === 0)
|
|
556
|
+
throw new Error(
|
|
557
|
+
`kotlin output no longer contains ${from}; recheck how it detects its host in Peach`,
|
|
558
|
+
)
|
|
441
559
|
}
|
|
442
|
-
if (patched === 0)
|
|
443
|
-
throw new Error(
|
|
444
|
-
'kotlin loader no longer reads process.release.name; recheck its node detection in Peach',
|
|
445
|
-
)
|
|
446
560
|
fs.renameSync(work, out)
|
|
447
561
|
return artifact
|
|
448
562
|
})().finally(() => builds.delete(hash))
|
|
@@ -502,13 +616,22 @@ export function peachKotlinModuleRoot(root: string): string {
|
|
|
502
616
|
export async function serveKotlinRequest(
|
|
503
617
|
pathname: string,
|
|
504
618
|
res: ServerResponse,
|
|
505
|
-
app: {
|
|
619
|
+
app: {
|
|
620
|
+
root: string
|
|
621
|
+
moduleRoot: string
|
|
622
|
+
origin: string
|
|
623
|
+
hmrPath: string
|
|
624
|
+
// a pure Kotlin app (peach kotlin); otherwise the root's imported sources
|
|
625
|
+
project?: KotlinAppProject
|
|
626
|
+
},
|
|
506
627
|
): Promise<boolean> {
|
|
507
628
|
if (serveKotlinArtifact(pathname, res)) return true
|
|
508
629
|
if (pathname !== KOTLIN_PACKAGE_PATH) return false
|
|
509
630
|
res.setHeader('access-control-allow-origin', '*')
|
|
510
631
|
try {
|
|
511
|
-
const artifact =
|
|
632
|
+
const artifact = app.project
|
|
633
|
+
? await kotlinAppArtifact(app.project, app.moduleRoot, app.origin)
|
|
634
|
+
: await kotlinArtifact(app.root, app.moduleRoot, app.origin)
|
|
512
635
|
res.setHeader('content-type', 'application/json')
|
|
513
636
|
res.end(JSON.stringify({ ...artifact, hmrPath: app.hmrPath }))
|
|
514
637
|
} catch (error) {
|
package/src/swift-compile.ts
CHANGED
|
@@ -529,6 +529,11 @@ export function writeSwiftProject(
|
|
|
529
529
|
): void {
|
|
530
530
|
const targets = compileTargets(input)
|
|
531
531
|
const files = new Map<string, string>()
|
|
532
|
+
// swiftpm plans a build root's build once and reuses the plan until a
|
|
533
|
+
// manifest changes, so a file added to the module would never compile
|
|
534
|
+
// there (RAN: a broken new UIKitCore file built "complete" in 0.1s). the
|
|
535
|
+
// module's file list in the manifest replans exactly when it changes.
|
|
536
|
+
const moduleFiles = `\n// sootsim-swift files ${createHash('sha256').update(swiftModuleInputFiles(moduleRoot).join('\n')).digest('hex').slice(0, 16)}\n`
|
|
532
537
|
if (targets.length === 0) {
|
|
533
538
|
for (const [file, contents] of Object.entries(input.files)) {
|
|
534
539
|
if (file === 'Package.swift') continue
|
|
@@ -552,7 +557,7 @@ export function writeSwiftProject(
|
|
|
552
557
|
.replace(product, `${product}\n ${ONE_PRODUCT},`)
|
|
553
558
|
.replace(flags, `${flags}${ONE_IMPLICIT_FLAGS}`)
|
|
554
559
|
}
|
|
555
|
-
files.set(path.join(layout.root, 'Package.swift'), manifest)
|
|
560
|
+
files.set(path.join(layout.root, 'Package.swift'), manifest + moduleFiles)
|
|
556
561
|
} else {
|
|
557
562
|
for (const [file, contents] of Object.entries(input.files)) {
|
|
558
563
|
if (file === 'Package.swift') continue
|
|
@@ -582,7 +587,7 @@ export function writeSwiftProject(
|
|
|
582
587
|
siblings: swiftSiblingDependencies(input.files, targets),
|
|
583
588
|
moduleRoot,
|
|
584
589
|
oneModule: input.oneModule,
|
|
585
|
-
}),
|
|
590
|
+
}) + moduleFiles,
|
|
586
591
|
)
|
|
587
592
|
}
|
|
588
593
|
syncSwiftTree(layout.root, files)
|