vxrn 2.14.2 → 2.15.1
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/dist/exports/prebuild.mjs +37 -0
- package/dist/exports/prebuild.mjs.map +1 -1
- package/dist/exports/prebuild.native.js +55 -0
- package/dist/exports/prebuild.native.js.map +1 -1
- package/dist/exports/prebuildWithoutExpo.mjs +221 -12
- package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
- package/dist/exports/prebuildWithoutExpo.native.js +244 -10
- package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
- package/package.json +13 -13
- package/src/exports/prebuild.ts +30 -0
- package/src/exports/prebuildWithoutExpo.test.ts +133 -2
- package/src/exports/prebuildWithoutExpo.ts +234 -12
- package/types/exports/prebuild.d.ts.map +1 -1
- package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vxrn",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -80,14 +80,14 @@
|
|
|
80
80
|
"@react-native-community/cli": "^20.2.0",
|
|
81
81
|
"@react-native-community/cli-config": "^20.2.0",
|
|
82
82
|
"@react-native/dev-middleware": "^0.88.0",
|
|
83
|
-
"@vxrn/compiler": "2.
|
|
84
|
-
"@vxrn/debug": "2.
|
|
85
|
-
"@vxrn/query-string": "2.
|
|
86
|
-
"@vxrn/resolve": "2.
|
|
87
|
-
"@vxrn/url-parse": "2.
|
|
88
|
-
"@vxrn/utils": "2.
|
|
89
|
-
"@vxrn/vendor": "2.
|
|
90
|
-
"@vxrn/vite-flow": "2.
|
|
83
|
+
"@vxrn/compiler": "2.15.1",
|
|
84
|
+
"@vxrn/debug": "2.15.1",
|
|
85
|
+
"@vxrn/query-string": "2.15.1",
|
|
86
|
+
"@vxrn/resolve": "2.15.1",
|
|
87
|
+
"@vxrn/url-parse": "2.15.1",
|
|
88
|
+
"@vxrn/utils": "2.15.1",
|
|
89
|
+
"@vxrn/vendor": "2.15.1",
|
|
90
|
+
"@vxrn/vite-flow": "2.15.1",
|
|
91
91
|
"citty": "^0.1.6",
|
|
92
92
|
"dotenv": "^17.2.1",
|
|
93
93
|
"dotenv-expand": "^12.0.2",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@biomejs/biome": "2.5.15",
|
|
114
114
|
"@jridgewell/gen-mapping": "^0.3.5",
|
|
115
|
-
"@tamagui/build": "3.
|
|
115
|
+
"@tamagui/build": "3.3.0",
|
|
116
116
|
"@types/node": "^24.10.0",
|
|
117
117
|
"@types/ws": "^8.18.1",
|
|
118
118
|
"depcheck": "^1.4.7",
|
|
@@ -121,20 +121,20 @@
|
|
|
121
121
|
"react-native": "0.88.0",
|
|
122
122
|
"typescript": "7.0.2",
|
|
123
123
|
"vitest": "^4.1.0",
|
|
124
|
-
"@vxrn/vite-plugin-metro": "2.
|
|
124
|
+
"@vxrn/vite-plugin-metro": "2.15.1"
|
|
125
125
|
},
|
|
126
126
|
"publishConfig": {
|
|
127
127
|
"access": "public"
|
|
128
128
|
},
|
|
129
129
|
"peerDependencies": {
|
|
130
|
-
"@vxrn/vite-plugin-metro": "2.
|
|
130
|
+
"@vxrn/vite-plugin-metro": "2.15.1"
|
|
131
131
|
},
|
|
132
132
|
"peerDependenciesMeta": {
|
|
133
133
|
"@vxrn/vite-plugin-metro": {
|
|
134
134
|
"optional": true
|
|
135
135
|
}
|
|
136
136
|
},
|
|
137
|
-
"releaseSourceCommit": "
|
|
137
|
+
"releaseSourceCommit": "84d72d51f9619c438190e70050d616a67c5f5bd1",
|
|
138
138
|
"repository": {
|
|
139
139
|
"type": "git",
|
|
140
140
|
"url": "git+https://github.com/onejs/one.git",
|
package/src/exports/prebuild.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { detectPackageManager, type PackageManagerName } from '@vxrn/utils'
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, renameSync, rmSync } from 'node:fs'
|
|
2
3
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
import { fillOptions } from '../config/getOptionsFilled'
|
|
@@ -64,6 +65,12 @@ export const prebuild = async ({
|
|
|
64
65
|
throw new Error('package.json checks are failing:\n' + error)
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
const ios = path.join(root, 'ios')
|
|
69
|
+
const previousIos = `${ios}.previous`
|
|
70
|
+
rmSync(previousIos, { recursive: true, force: true })
|
|
71
|
+
const reusesIos = (platform == 'ios' || !platform) && existsSync(ios)
|
|
72
|
+
if (reusesIos) renameSync(ios, previousIos)
|
|
73
|
+
|
|
67
74
|
if (platform == 'ios' || !platform) {
|
|
68
75
|
await generateForPlatform(root, 'ios', app)
|
|
69
76
|
}
|
|
@@ -73,6 +80,8 @@ export const prebuild = async ({
|
|
|
73
80
|
|
|
74
81
|
await beforeInstall?.()
|
|
75
82
|
|
|
83
|
+
if (reusesIos) reuseUnchangedIosProject(previousIos, ios)
|
|
84
|
+
|
|
76
85
|
// community config reads the generated projects, so discover after generation
|
|
77
86
|
// and before installation. `--no-install` never fakes the linked package set.
|
|
78
87
|
const inventory = await getNativeDependencyInventory(root)
|
|
@@ -121,6 +130,27 @@ Android:
|
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
// xcode keys each build input on its file identity and modification time, so a
|
|
134
|
+
// regenerated file it already compiled must be the same file, not equal bytes.
|
|
135
|
+
// what pod install and xcodebuild wrote stays too, so both resume incrementally.
|
|
136
|
+
function reuseUnchangedIosProject(previous: string, next: string) {
|
|
137
|
+
for (const entry of readdirSync(next, { recursive: true, withFileTypes: true })) {
|
|
138
|
+
if (!entry.isFile()) continue
|
|
139
|
+
const file = path.join(entry.parentPath, entry.name)
|
|
140
|
+
const old = path.join(previous, path.relative(next, file))
|
|
141
|
+
if (!existsSync(old) || !lstatSync(old).isFile()) continue
|
|
142
|
+
if (lstatSync(old).size !== lstatSync(file).size) continue
|
|
143
|
+
if (!readFileSync(old).equals(readFileSync(file))) continue
|
|
144
|
+
renameSync(old, file)
|
|
145
|
+
}
|
|
146
|
+
for (const name of ['Pods', 'Podfile.lock', 'build']) {
|
|
147
|
+
const from = path.join(previous, name)
|
|
148
|
+
const to = path.join(next, name)
|
|
149
|
+
if (existsSync(from) && !existsSync(to)) renameSync(from, to)
|
|
150
|
+
}
|
|
151
|
+
rmSync(previous, { recursive: true, force: true })
|
|
152
|
+
}
|
|
153
|
+
|
|
124
154
|
export async function replaceInUTF8File(
|
|
125
155
|
filePath: string,
|
|
126
156
|
findThis: string,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
generateForPlatform,
|
|
24
24
|
generateKotlinSources,
|
|
25
25
|
getNativeDependencyInventory,
|
|
26
|
+
renderIosAppIntentsSwift,
|
|
26
27
|
renderPrebuildFile,
|
|
27
28
|
renderSceneDelegateSwift,
|
|
28
29
|
type PrebuildAppConfig,
|
|
@@ -1286,6 +1287,65 @@ describe('ios scene lifecycle', () => {
|
|
|
1286
1287
|
expect(sceneDelegate).toContain('RCTLinkingManager.scene(scene, continue: userActivity)')
|
|
1287
1288
|
})
|
|
1288
1289
|
|
|
1290
|
+
it('runs native.app.patches on the generated files and fails on a missing anchor', async () => {
|
|
1291
|
+
const workspaceRoot = fileURLToPath(new URL('../../../..', import.meta.url))
|
|
1292
|
+
const output = mkdtempSync(join(tmpdir(), 'vxrn-prebuild-patches-'))
|
|
1293
|
+
const replaceOnce = (from: string, to: string) => (content: string) => {
|
|
1294
|
+
if (content.includes(to)) return content
|
|
1295
|
+
if (!content.includes(from)) throw new Error(`missing anchor: ${from}`)
|
|
1296
|
+
return content.replace(from, to)
|
|
1297
|
+
}
|
|
1298
|
+
const patches = [
|
|
1299
|
+
{
|
|
1300
|
+
platform: 'ios' as const,
|
|
1301
|
+
file: 'AppDelegate.swift',
|
|
1302
|
+
patch: replaceOnce(
|
|
1303
|
+
'Bundle.main.url(forResource: "main", withExtension: "jsbundle")',
|
|
1304
|
+
'HotUpdater.bundleURL()'
|
|
1305
|
+
),
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
platform: 'ios' as const,
|
|
1309
|
+
file: 'MyApp/Info.plist',
|
|
1310
|
+
patch: replaceOnce('<dict>', '<dict>\n\t<key>HotUpdaterChannel</key>\n\t<string>production</string>'),
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
platform: 'android' as const,
|
|
1314
|
+
file: 'MainApplication.kt',
|
|
1315
|
+
patch: replaceOnce(
|
|
1316
|
+
'getDefaultReactHost(\n context = applicationContext,',
|
|
1317
|
+
'getDefaultReactHost(\n context = applicationContext,\n jsBundleFilePath = HotUpdater.getJSBundleFile(applicationContext),'
|
|
1318
|
+
),
|
|
1319
|
+
},
|
|
1320
|
+
]
|
|
1321
|
+
await generateForPlatform(workspaceRoot, 'ios', { ...app, patches }, join(output, 'ios'))
|
|
1322
|
+
await generateForPlatform(workspaceRoot, 'android', { ...app, patches }, join(output, 'android'))
|
|
1323
|
+
expect(readFileSync(join(output, 'ios', 'MyApp', 'AppDelegate.swift'), 'utf8')).toContain(
|
|
1324
|
+
'HotUpdater.bundleURL()'
|
|
1325
|
+
)
|
|
1326
|
+
expect(readFileSync(join(output, 'ios', 'MyApp', 'Info.plist'), 'utf8')).toContain(
|
|
1327
|
+
'HotUpdaterChannel'
|
|
1328
|
+
)
|
|
1329
|
+
expect(
|
|
1330
|
+
readFileSync(
|
|
1331
|
+
join(output, 'android', 'app/src/main/java/dev/one/myapp/MainApplication.kt'),
|
|
1332
|
+
'utf8'
|
|
1333
|
+
)
|
|
1334
|
+
).toContain('jsBundleFilePath = HotUpdater.getJSBundleFile(applicationContext)')
|
|
1335
|
+
|
|
1336
|
+
await expect(
|
|
1337
|
+
generateForPlatform(
|
|
1338
|
+
workspaceRoot,
|
|
1339
|
+
'ios',
|
|
1340
|
+
{
|
|
1341
|
+
...app,
|
|
1342
|
+
patches: [{ platform: 'ios', file: 'AppDelegate.swift', patch: replaceOnce('nope-anchor', 'replacement-text') }],
|
|
1343
|
+
},
|
|
1344
|
+
join(output, 'ios2')
|
|
1345
|
+
)
|
|
1346
|
+
).rejects.toThrow('missing anchor')
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1289
1349
|
it('bundles native.app fonts into both platforms from the real template', async () => {
|
|
1290
1350
|
const workspaceRoot = fileURLToPath(new URL('../../../..', import.meta.url))
|
|
1291
1351
|
const output = mkdtempSync(join(tmpdir(), 'vxrn-prebuild-fonts-'))
|
|
@@ -2139,9 +2199,12 @@ describe('ios widgets', () => {
|
|
|
2139
2199
|
'rectangle',
|
|
2140
2200
|
'rounded-rectangle',
|
|
2141
2201
|
'link',
|
|
2202
|
+
'button',
|
|
2142
2203
|
]) {
|
|
2143
2204
|
expect(rendered).toContain(`case "${type}":`)
|
|
2144
2205
|
}
|
|
2206
|
+
expect(rendered).toContain('struct OneWidgetButtonIntent: AppIntent')
|
|
2207
|
+
expect(rendered).toContain('.supportedFamilies([.systemSmall, .systemMedium])')
|
|
2145
2208
|
// every ActivityView slot needs a decode path in both presentations
|
|
2146
2209
|
for (const slot of [
|
|
2147
2210
|
'lockScreen',
|
|
@@ -2155,13 +2218,81 @@ describe('ios widgets', () => {
|
|
|
2155
2218
|
expect(rendered).toContain(slot)
|
|
2156
2219
|
}
|
|
2157
2220
|
// the JSX bridge methods exist on both sides of the React Native bridge
|
|
2158
|
-
for (const method of ['writeView', 'startView', 'updateView']) {
|
|
2221
|
+
for (const method of ['writeView', 'startView', 'updateView', 'claimActions']) {
|
|
2159
2222
|
expect(readGenerated('MyApp/OneWidgetsBridge.swift')).toContain(method)
|
|
2160
2223
|
expect(readGenerated('MyApp/OneWidgetsBridge.m')).toContain(method)
|
|
2161
2224
|
}
|
|
2162
|
-
|
|
2225
|
+
expect(readGenerated('MyApp/OneWidgetsBridge.swift')).toContain('"oneWidgetAction"')
|
|
2226
|
+
// the shared contract carries the serialized layouts and interactive actions
|
|
2163
2227
|
const contract = readGenerated('MyApp/OneWidgetContract.swift')
|
|
2164
2228
|
expect(contract.match(/let layout: String\?/g)).toHaveLength(2)
|
|
2229
|
+
expect(contract).toContain('static let actionKeyPrefix = "one.widget.action."')
|
|
2230
|
+
expect(contract).toContain('struct Action: Codable')
|
|
2231
|
+
expect(rendered).toContain('OneWidgetContract.actionKeyPrefix + item.id')
|
|
2232
|
+
expect(rendered).toContain('OneWidgetButtonIntent(action: action, value: node.value?.string)')
|
|
2233
|
+
expect(readGenerated('MyApp/OneWidgetsBridge.swift')).toContain(
|
|
2234
|
+
'key.hasPrefix(OneWidgetContract.actionKeyPrefix)'
|
|
2235
|
+
)
|
|
2236
|
+
}, 180000)
|
|
2237
|
+
|
|
2238
|
+
it('emits configured widget families and App Intents spoken dialog metadata', async () => {
|
|
2239
|
+
const workspaceRoot = fileURLToPath(new URL('../../../..', import.meta.url))
|
|
2240
|
+
const output = mkdtempSync(join(tmpdir(), 'vxrn-prebuild-widgets-families-'))
|
|
2241
|
+
await generateForPlatform(
|
|
2242
|
+
workspaceRoot,
|
|
2243
|
+
'ios',
|
|
2244
|
+
{
|
|
2245
|
+
...widgetsApp,
|
|
2246
|
+
ios: {
|
|
2247
|
+
...widgetsApp.ios,
|
|
2248
|
+
widgets: {
|
|
2249
|
+
...widgetsApp.ios.widgets,
|
|
2250
|
+
families: ['systemSmall', 'systemLarge', 'accessoryCircular', 'accessoryRectangular'],
|
|
2251
|
+
},
|
|
2252
|
+
},
|
|
2253
|
+
},
|
|
2254
|
+
join(output, 'ios')
|
|
2255
|
+
)
|
|
2256
|
+
const renderedWidget = readFileSync(
|
|
2257
|
+
join(output, 'ios', 'OneWidgets', 'OneWidget.swift'),
|
|
2258
|
+
'utf8'
|
|
2259
|
+
)
|
|
2260
|
+
expect(renderedWidget).toContain(
|
|
2261
|
+
'.supportedFamilies([.systemSmall, .systemLarge, .accessoryCircular, .accessoryRectangular])'
|
|
2262
|
+
)
|
|
2263
|
+
|
|
2264
|
+
const swift = renderIosAppIntentsSwift([
|
|
2265
|
+
{
|
|
2266
|
+
id: 'dev.one.myapp.ask',
|
|
2267
|
+
title: 'Ask assistant',
|
|
2268
|
+
description: 'Run a voice query and speak the answer.',
|
|
2269
|
+
textParameterTitle: 'Prompt',
|
|
2270
|
+
shortcutPhrase: 'Ask {app} a question',
|
|
2271
|
+
shortcutPhrases: ['Query {app}', 'Ask {app} a question'],
|
|
2272
|
+
systemImageName: 'waveform.circle.fill',
|
|
2273
|
+
openAppWhenRun: true,
|
|
2274
|
+
},
|
|
2275
|
+
{
|
|
2276
|
+
id: 'dev.one.myapp.ping',
|
|
2277
|
+
title: 'Ping',
|
|
2278
|
+
shortcutPhrase: 'Ping {app}',
|
|
2279
|
+
},
|
|
2280
|
+
])
|
|
2281
|
+
expect(swift).toContain(
|
|
2282
|
+
'func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog'
|
|
2283
|
+
)
|
|
2284
|
+
expect(swift).toContain(
|
|
2285
|
+
'return .result(value: result, dialog: IntentDialog(stringLiteral: result))'
|
|
2286
|
+
)
|
|
2287
|
+
expect(swift).toContain(
|
|
2288
|
+
'static let description = IntentDescription("Run a voice query and speak the answer.")'
|
|
2289
|
+
)
|
|
2290
|
+
expect(swift).toContain('static let openAppWhenRun: Bool = true')
|
|
2291
|
+
expect(swift).toContain(
|
|
2292
|
+
'phrases: ["Ask \\(.applicationName) a question", "Query \\(.applicationName)"]'
|
|
2293
|
+
)
|
|
2294
|
+
expect(swift).toContain('systemImageName: "waveform.circle.fill"')
|
|
2295
|
+
expect(swift).toContain('systemImageName: "bolt.fill"')
|
|
2165
2296
|
}, 180000)
|
|
2166
2297
|
|
|
2167
2298
|
it('regenerates byte-identical widget projects and adds nothing without the config', async () => {
|
|
@@ -372,6 +372,12 @@ function swiftShortcutPhrase(value: string): string {
|
|
|
372
372
|
export function renderIosAppIntentsSwift(actions: NonNullable<NonNullable<NativeAppManifest['ios']>['appIntents']>['actions']): string {
|
|
373
373
|
const types = actions.map((action, index) => {
|
|
374
374
|
const summaryTitle = swiftLiteral(action.title).slice(1, -1)
|
|
375
|
+
const description =
|
|
376
|
+
action.description === undefined
|
|
377
|
+
? ''
|
|
378
|
+
: `\n static let description = IntentDescription(${swiftLiteral(action.description)})`
|
|
379
|
+
const openAppWhenRun =
|
|
380
|
+
action.openAppWhenRun === true ? '\n static let openAppWhenRun: Bool = true' : ''
|
|
375
381
|
const text = action.textParameterTitle === undefined ? '' : `
|
|
376
382
|
@Parameter(title: ${swiftLiteral(action.textParameterTitle)}) var text: String
|
|
377
383
|
|
|
@@ -380,20 +386,25 @@ export function renderIosAppIntentsSwift(actions: NonNullable<NonNullable<Native
|
|
|
380
386
|
}
|
|
381
387
|
`
|
|
382
388
|
return `struct OneAppIntent${index}: AppIntent {
|
|
383
|
-
static let title: LocalizedStringResource = ${swiftLiteral(action.title)}
|
|
389
|
+
static let title: LocalizedStringResource = ${swiftLiteral(action.title)}${description}${openAppWhenRun}
|
|
384
390
|
${text}
|
|
385
|
-
func perform() async throws -> some IntentResult & ReturnsValue<String> {
|
|
391
|
+
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
|
|
386
392
|
let result = try await oneAppIntentsRun(${swiftLiteral(action.id)}, ${text ? 'text' : 'nil'})
|
|
387
|
-
return .result(value: result)
|
|
393
|
+
return .result(value: result, dialog: IntentDialog(stringLiteral: result))
|
|
388
394
|
}
|
|
389
395
|
}`
|
|
390
396
|
})
|
|
391
|
-
const shortcuts = actions.map((action, index) =>
|
|
397
|
+
const shortcuts = actions.map((action, index) => {
|
|
398
|
+
const phrases = Array.from(
|
|
399
|
+
new Set([action.shortcutPhrase, ...(action.shortcutPhrases ?? [])])
|
|
400
|
+
).map(swiftShortcutPhrase)
|
|
401
|
+
return ` AppShortcut(
|
|
392
402
|
intent: OneAppIntent${index}(),
|
|
393
|
-
phrases: [${
|
|
403
|
+
phrases: [${phrases.join(', ')}],
|
|
394
404
|
shortTitle: ${swiftLiteral(action.title)},
|
|
395
|
-
systemImageName:
|
|
396
|
-
)`
|
|
405
|
+
systemImageName: ${swiftLiteral(action.systemImageName ?? 'bolt.fill')}
|
|
406
|
+
)`
|
|
407
|
+
}).join('\n')
|
|
397
408
|
return `import AppIntents
|
|
398
409
|
import Foundation
|
|
399
410
|
|
|
@@ -871,6 +882,9 @@ function generateIosWidgets(dest: string, app: NativeAppManifest): void {
|
|
|
871
882
|
</dict></plist>
|
|
872
883
|
`
|
|
873
884
|
)
|
|
885
|
+
const supportedFamilies = (widgets.families ?? ['systemSmall', 'systemMedium'])
|
|
886
|
+
.map((family) => `.${family}`)
|
|
887
|
+
.join(', ')
|
|
874
888
|
writeFileSync(
|
|
875
889
|
path.join(appDir, 'OneWidgetContract.swift'),
|
|
876
890
|
`import Foundation
|
|
@@ -880,6 +894,7 @@ enum OneWidgetContract {
|
|
|
880
894
|
static let appGroup = ${JSON.stringify(widgets.appGroup)}
|
|
881
895
|
static let kind = ${JSON.stringify(widgets.kind)}
|
|
882
896
|
static let dataKey = "one.widget.data"
|
|
897
|
+
static let actionKeyPrefix = "one.widget.action."
|
|
883
898
|
|
|
884
899
|
struct Data: Codable {
|
|
885
900
|
let title: String
|
|
@@ -888,6 +903,13 @@ enum OneWidgetContract {
|
|
|
888
903
|
let layout: String?
|
|
889
904
|
}
|
|
890
905
|
|
|
906
|
+
struct Action: Codable {
|
|
907
|
+
let id: String
|
|
908
|
+
let action: String
|
|
909
|
+
let value: String?
|
|
910
|
+
let timestampMs: Double
|
|
911
|
+
}
|
|
912
|
+
|
|
891
913
|
struct Attributes: ActivityAttributes {
|
|
892
914
|
struct ContentState: Codable, Hashable {
|
|
893
915
|
let status: String
|
|
@@ -904,6 +926,7 @@ enum OneWidgetContract {
|
|
|
904
926
|
`import SwiftUI
|
|
905
927
|
import WidgetKit
|
|
906
928
|
import ActivityKit
|
|
929
|
+
import AppIntents
|
|
907
930
|
|
|
908
931
|
struct OneWidgetNode: Decodable {
|
|
909
932
|
let type: String
|
|
@@ -911,11 +934,36 @@ struct OneWidgetNode: Decodable {
|
|
|
911
934
|
let style: Style?
|
|
912
935
|
let children: [OneWidgetNode]?
|
|
913
936
|
let systemName: String?
|
|
914
|
-
let value:
|
|
937
|
+
let value: Value?
|
|
915
938
|
let total: Double?
|
|
916
939
|
let fill: String?
|
|
917
940
|
let cornerRadius: Double?
|
|
918
941
|
let url: String?
|
|
942
|
+
let action: String?
|
|
943
|
+
|
|
944
|
+
enum Value: Decodable {
|
|
945
|
+
case number(Double)
|
|
946
|
+
case string(String)
|
|
947
|
+
|
|
948
|
+
var number: Double? {
|
|
949
|
+
if case .number(let value) = self { return value }
|
|
950
|
+
return nil
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
var string: String? {
|
|
954
|
+
if case .string(let value) = self { return value }
|
|
955
|
+
return nil
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
init(from decoder: Decoder) throws {
|
|
959
|
+
let container = try decoder.singleValueContainer()
|
|
960
|
+
if let number = try? container.decode(Double.self) {
|
|
961
|
+
self = .number(number)
|
|
962
|
+
} else {
|
|
963
|
+
self = .string(try container.decode(String.self))
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
919
967
|
|
|
920
968
|
struct Style: Decodable {
|
|
921
969
|
let color: String?
|
|
@@ -934,6 +982,43 @@ struct OneWidgetNode: Decodable {
|
|
|
934
982
|
}
|
|
935
983
|
}
|
|
936
984
|
|
|
985
|
+
struct OneWidgetButtonIntent: AppIntent {
|
|
986
|
+
static var title: LocalizedStringResource = "Widget Action"
|
|
987
|
+
|
|
988
|
+
@Parameter(title: "Action") var action: String
|
|
989
|
+
@Parameter(title: "Value") var value: String?
|
|
990
|
+
|
|
991
|
+
init() {}
|
|
992
|
+
|
|
993
|
+
init(action: String, value: String?) {
|
|
994
|
+
self.action = action
|
|
995
|
+
self.value = value
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
func perform() async throws -> some IntentResult {
|
|
999
|
+
if let defaults = UserDefaults(suiteName: OneWidgetContract.appGroup) {
|
|
1000
|
+
let item = OneWidgetContract.Action(
|
|
1001
|
+
id: UUID().uuidString,
|
|
1002
|
+
action: action,
|
|
1003
|
+
value: value,
|
|
1004
|
+
timestampMs: Date().timeIntervalSince1970 * 1000
|
|
1005
|
+
)
|
|
1006
|
+
if let encoded = try? JSONEncoder().encode(item) {
|
|
1007
|
+
defaults.set(encoded, forKey: OneWidgetContract.actionKeyPrefix + item.id)
|
|
1008
|
+
}
|
|
1009
|
+
CFNotificationCenterPostNotification(
|
|
1010
|
+
CFNotificationCenterGetDarwinNotifyCenter(),
|
|
1011
|
+
CFNotificationName("dev.onejs.widget.action" as CFString),
|
|
1012
|
+
nil,
|
|
1013
|
+
nil,
|
|
1014
|
+
true
|
|
1015
|
+
)
|
|
1016
|
+
}
|
|
1017
|
+
WidgetCenter.shared.reloadTimelines(ofKind: OneWidgetContract.kind)
|
|
1018
|
+
return .result()
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
937
1022
|
struct OneActivityView: Decodable {
|
|
938
1023
|
let lockScreen: OneWidgetNode
|
|
939
1024
|
let compactLeading: OneWidgetNode?
|
|
@@ -1026,10 +1111,10 @@ struct OneWidgetRendered: View {
|
|
|
1026
1111
|
.font(.system(size: CGFloat(node.style?.fontSize ?? 20), weight: weight))
|
|
1027
1112
|
}
|
|
1028
1113
|
case "progress":
|
|
1029
|
-
ProgressView(value: node.value ?? 0, total: node.total ?? 1)
|
|
1114
|
+
ProgressView(value: node.value?.number ?? 0, total: node.total ?? 1)
|
|
1030
1115
|
.tint(oneColor(node.style?.color))
|
|
1031
1116
|
case "gauge":
|
|
1032
|
-
Gauge(value: node.value ?? 0, in: 0...(node.total ?? 1)) { EmptyView() }
|
|
1117
|
+
Gauge(value: node.value?.number ?? 0, in: 0...(node.total ?? 1)) { EmptyView() }
|
|
1033
1118
|
.gaugeStyle(.accessoryCircular)
|
|
1034
1119
|
.tint(oneColor(node.style?.color))
|
|
1035
1120
|
case "circle":
|
|
@@ -1047,6 +1132,15 @@ struct OneWidgetRendered: View {
|
|
|
1047
1132
|
}
|
|
1048
1133
|
}
|
|
1049
1134
|
}
|
|
1135
|
+
case "button":
|
|
1136
|
+
if let action = node.action {
|
|
1137
|
+
Button(intent: OneWidgetButtonIntent(action: action, value: node.value?.string)) {
|
|
1138
|
+
ForEach(Array((node.children ?? []).enumerated()), id: \\.offset) { _, child in
|
|
1139
|
+
OneWidgetRendered(node: child)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
.buttonStyle(.plain)
|
|
1143
|
+
}
|
|
1050
1144
|
default: EmptyView()
|
|
1051
1145
|
}
|
|
1052
1146
|
}
|
|
@@ -1107,7 +1201,7 @@ struct OneWidget: Widget {
|
|
|
1107
1201
|
}
|
|
1108
1202
|
.configurationDisplayName(${JSON.stringify(widgets.displayName)})
|
|
1109
1203
|
.description(${JSON.stringify(widgets.description)})
|
|
1110
|
-
.supportedFamilies([
|
|
1204
|
+
.supportedFamilies([${supportedFamilies}])
|
|
1111
1205
|
}
|
|
1112
1206
|
}
|
|
1113
1207
|
|
|
@@ -1204,31 +1298,120 @@ RCT_EXTERN_METHOD(update:(NSString *)identifier status:(NSString *)status value:
|
|
|
1204
1298
|
RCT_EXTERN_METHOD(updateView:(NSString *)identifier layout:(NSString *)layout resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
1205
1299
|
RCT_EXTERN_METHOD(end:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
1206
1300
|
RCT_EXTERN_METHOD(pushToken:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
1301
|
+
RCT_EXTERN_METHOD(claimActions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
1207
1302
|
@end
|
|
1208
1303
|
`
|
|
1209
1304
|
)
|
|
1210
1305
|
writeFileSync(
|
|
1211
1306
|
path.join(appDir, 'OneWidgetsBridge.swift'),
|
|
1212
1307
|
`import Foundation
|
|
1308
|
+
import UIKit
|
|
1213
1309
|
import React
|
|
1214
1310
|
import WidgetKit
|
|
1215
1311
|
import ActivityKit
|
|
1216
1312
|
|
|
1313
|
+
private func oneWidgetsDarwinCallback(
|
|
1314
|
+
_ center: CFNotificationCenter?,
|
|
1315
|
+
_ observer: UnsafeMutableRawPointer?,
|
|
1316
|
+
_ name: CFNotificationName?,
|
|
1317
|
+
_ object: UnsafeRawPointer?,
|
|
1318
|
+
_ userInfo: CFDictionary?
|
|
1319
|
+
) {
|
|
1320
|
+
guard let observer else { return }
|
|
1321
|
+
let bridge = Unmanaged<OneWidgetsBridge>.fromOpaque(observer).takeUnretainedValue()
|
|
1322
|
+
bridge.flushObservedActions()
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1217
1325
|
@objc(OneWidgetsBridge)
|
|
1218
1326
|
class OneWidgetsBridge: RCTEventEmitter {
|
|
1219
1327
|
private var tokenTasks: [String: Task<Void, Never>] = [:]
|
|
1328
|
+
private var isObservingEvents = false
|
|
1329
|
+
private var foregroundObserver: NSObjectProtocol?
|
|
1330
|
+
private let actionLock = NSLock()
|
|
1220
1331
|
|
|
1221
|
-
override func supportedEvents() -> [String]! { ["oneLiveActivityPushToken"] }
|
|
1332
|
+
override func supportedEvents() -> [String]! { ["oneLiveActivityPushToken", "oneWidgetAction"] }
|
|
1222
1333
|
|
|
1223
1334
|
override func startObserving() {
|
|
1335
|
+
isObservingEvents = true
|
|
1224
1336
|
for activity in Activity<OneWidgetContract.Attributes>.activities {
|
|
1225
1337
|
observeToken(activity)
|
|
1226
1338
|
}
|
|
1339
|
+
CFNotificationCenterAddObserver(
|
|
1340
|
+
CFNotificationCenterGetDarwinNotifyCenter(),
|
|
1341
|
+
Unmanaged.passUnretained(self).toOpaque(),
|
|
1342
|
+
oneWidgetsDarwinCallback,
|
|
1343
|
+
"dev.onejs.widget.action" as CFString,
|
|
1344
|
+
nil,
|
|
1345
|
+
.deliverImmediately
|
|
1346
|
+
)
|
|
1347
|
+
foregroundObserver = NotificationCenter.default.addObserver(
|
|
1348
|
+
forName: UIApplication.didBecomeActiveNotification,
|
|
1349
|
+
object: nil,
|
|
1350
|
+
queue: .main
|
|
1351
|
+
) { [weak self] _ in
|
|
1352
|
+
self?.flushObservedActions()
|
|
1353
|
+
}
|
|
1354
|
+
flushObservedActions()
|
|
1227
1355
|
}
|
|
1228
1356
|
|
|
1229
1357
|
override func stopObserving() {
|
|
1358
|
+
isObservingEvents = false
|
|
1230
1359
|
for task in tokenTasks.values { task.cancel() }
|
|
1231
1360
|
tokenTasks.removeAll()
|
|
1361
|
+
CFNotificationCenterRemoveObserver(
|
|
1362
|
+
CFNotificationCenterGetDarwinNotifyCenter(),
|
|
1363
|
+
Unmanaged.passUnretained(self).toOpaque(),
|
|
1364
|
+
CFNotificationName("dev.onejs.widget.action" as CFString),
|
|
1365
|
+
nil
|
|
1366
|
+
)
|
|
1367
|
+
if let foregroundObserver {
|
|
1368
|
+
NotificationCenter.default.removeObserver(foregroundObserver)
|
|
1369
|
+
self.foregroundObserver = nil
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
deinit {
|
|
1374
|
+
CFNotificationCenterRemoveObserver(
|
|
1375
|
+
CFNotificationCenterGetDarwinNotifyCenter(),
|
|
1376
|
+
Unmanaged.passUnretained(self).toOpaque(),
|
|
1377
|
+
CFNotificationName("dev.onejs.widget.action" as CFString),
|
|
1378
|
+
nil
|
|
1379
|
+
)
|
|
1380
|
+
if let foregroundObserver {
|
|
1381
|
+
NotificationCenter.default.removeObserver(foregroundObserver)
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
fileprivate func flushObservedActions() {
|
|
1386
|
+
guard isObservingEvents else { return }
|
|
1387
|
+
for item in drainStoredActions() {
|
|
1388
|
+
sendEvent(withName: "oneWidgetAction", body: item)
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
private func drainStoredActions() -> [[String: Any]] {
|
|
1393
|
+
actionLock.lock()
|
|
1394
|
+
defer { actionLock.unlock() }
|
|
1395
|
+
guard let defaults = UserDefaults(suiteName: OneWidgetContract.appGroup) else {
|
|
1396
|
+
return []
|
|
1397
|
+
}
|
|
1398
|
+
var actions: [OneWidgetContract.Action] = []
|
|
1399
|
+
for key in defaults.dictionaryRepresentation().keys where key.hasPrefix(OneWidgetContract.actionKeyPrefix) {
|
|
1400
|
+
guard let data = defaults.data(forKey: key) else { continue }
|
|
1401
|
+
defaults.removeObject(forKey: key)
|
|
1402
|
+
if let action = try? JSONDecoder().decode(OneWidgetContract.Action.self, from: data) {
|
|
1403
|
+
actions.append(action)
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
actions.sort { $0.timestampMs < $1.timestampMs }
|
|
1407
|
+
return actions.map { action in
|
|
1408
|
+
[
|
|
1409
|
+
"id": action.id,
|
|
1410
|
+
"action": action.action,
|
|
1411
|
+
"value": action.value ?? NSNull(),
|
|
1412
|
+
"timestampMs": action.timestampMs,
|
|
1413
|
+
]
|
|
1414
|
+
}
|
|
1232
1415
|
}
|
|
1233
1416
|
|
|
1234
1417
|
private func observeToken(_ activity: Activity<OneWidgetContract.Attributes>) {
|
|
@@ -1370,6 +1553,15 @@ class OneWidgetsBridge: RCTEventEmitter {
|
|
|
1370
1553
|
resolve(NSNull())
|
|
1371
1554
|
}
|
|
1372
1555
|
}
|
|
1556
|
+
|
|
1557
|
+
@objc(claimActions:rejecter:)
|
|
1558
|
+
func claimActions(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
|
|
1559
|
+
guard UserDefaults(suiteName: OneWidgetContract.appGroup) != nil else {
|
|
1560
|
+
reject("app_group", "Cannot open the configured App Group", nil)
|
|
1561
|
+
return
|
|
1562
|
+
}
|
|
1563
|
+
resolve(drainStoredActions())
|
|
1564
|
+
}
|
|
1373
1565
|
}
|
|
1374
1566
|
`
|
|
1375
1567
|
)
|
|
@@ -2972,6 +3164,36 @@ export const generateForPlatform = async (
|
|
|
2972
3164
|
if (platform === 'ios') await generateSwiftPackages({ root, dest })
|
|
2973
3165
|
if (platform === 'android') generateAndroidIconForwarder(dest, app)
|
|
2974
3166
|
if (platform === 'android') await generateKotlinSources({ root, dest })
|
|
3167
|
+
applyAppPatches(dest, platform, app)
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function listFiles(dir: string): string[] {
|
|
3171
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
|
|
3172
|
+
entry.isDirectory() ? listFiles(path.join(dir, entry.name)) : [path.join(dir, entry.name)]
|
|
3173
|
+
)
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
function applyAppPatches(dest: string, platform: 'ios' | 'android', app: NativeAppManifest) {
|
|
3177
|
+
const patches = (app.patches ?? []).filter((entry) => entry.platform === platform)
|
|
3178
|
+
if (patches.length === 0) return
|
|
3179
|
+
const files = listFiles(dest)
|
|
3180
|
+
for (const { file, patch } of patches) {
|
|
3181
|
+
const matches = files.filter((candidate) => {
|
|
3182
|
+
const relative = path.relative(dest, candidate).split(path.sep).join('/')
|
|
3183
|
+
return relative === file || relative.endsWith('/' + file)
|
|
3184
|
+
})
|
|
3185
|
+
if (matches.length !== 1) {
|
|
3186
|
+
throw new Error(
|
|
3187
|
+
`[vxrn] native.app.patches: "${file}" matched ${matches.length} ${platform} files, expected exactly one`
|
|
3188
|
+
)
|
|
3189
|
+
}
|
|
3190
|
+
const before = readFileSync(matches[0], 'utf8')
|
|
3191
|
+
const after = patch(before)
|
|
3192
|
+
if (patch(after) !== after) {
|
|
3193
|
+
throw new Error(`[vxrn] native.app.patches: patch for "${file}" is not idempotent`)
|
|
3194
|
+
}
|
|
3195
|
+
writeFileSync(matches[0], after)
|
|
3196
|
+
}
|
|
2975
3197
|
}
|
|
2976
3198
|
|
|
2977
3199
|
// an imported kotlin source can reach past the base ui/foundation/material3
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prebuild.d.ts","sourceRoot":"","sources":["../../src/exports/prebuild.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prebuild.d.ts","sourceRoot":"","sources":["../../src/exports/prebuild.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAA;AAK9B,eAAO,MAAM,QAAQ,qEAMlB;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAA;IACrC,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,GAAG,EAAE,iBAAiB,CAAA;IAGtB,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC,kBAkGA,CAAA;AAuBD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,iBAOpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prebuildWithoutExpo.d.ts","sourceRoot":"","sources":["../../src/exports/prebuildWithoutExpo.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,iBAAiB,EAEjB,KAAK,iBAAiB,EAEvB,MAAM,+BAA+B,CAAA;AAwEtC,YAAY,EAAE,iBAAiB,IAAI,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AAE3F,eAAO,MAAM,mBAAmB,0BAAoB,CAAA;AA6EpD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAwChE;AAyJD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,CAAC,WAAW,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"prebuildWithoutExpo.d.ts","sourceRoot":"","sources":["../../src/exports/prebuildWithoutExpo.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,iBAAiB,EAEjB,KAAK,iBAAiB,EAEvB,MAAM,+BAA+B,CAAA;AAwEtC,YAAY,EAAE,iBAAiB,IAAI,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AAE3F,eAAO,MAAM,mBAAmB,0BAAoB,CAAA;AA6EpD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAwChE;AAyJD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,CAAC,WAAW,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,MAAM,CAyDrI;AAqnCD,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,EAAE,MAAM,CAAA;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AA6sBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAA;IAC3B,GAAG,EAAE,iBAAiB,CAAA;IACtB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,GAAG,oBAAoB,CA0xBvB;AAED,eAAO,MAAM,mBAAmB,SACxB,MAAM,YACF,KAAK,GAAG,SAAS,OACtB,iBAAiB,WACd,MAAM,kBAwEf,CAAA;AA6DD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,QA6EvD;AAID,wBAAsB,qBAAqB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,iBAsDzF;AAyFD,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,iBAAiB,CAAA;IACtB,SAAS,EAAE,SAAS,yBAAyB,EAAE,CAAA;CAChD,GAAG,IAAI,CAmCP;AAID,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,yBAAyB,EAAE,CAAC,CA+BtC;AAKD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAA;CACtC,GAAG,IAAI,CAaP"}
|