@use-voltra/expo-plugin 2.2.0 → 2.3.0
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/build/cjs/constants.js +1 -3
- package/build/cjs/constants.js.map +1 -1
- package/build/cjs/dynamic-live-activity.js +6 -0
- package/build/cjs/dynamic-live-activity.js.map +1 -0
- package/build/cjs/index.js +15 -2
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/serverUpdate.js +119 -0
- package/build/cjs/serverUpdate.js.map +1 -0
- package/build/cjs/utils/packageVersion.js +49 -0
- package/build/cjs/utils/packageVersion.js.map +1 -0
- package/build/cjs/utils/prerender.js +25 -159
- package/build/cjs/utils/prerender.js.map +1 -1
- package/build/cjs/widgetServerUpdate.js +63 -0
- package/build/cjs/widgetServerUpdate.js.map +1 -0
- package/build/esm/constants.js +0 -2
- package/build/esm/constants.js.map +1 -1
- package/build/esm/dynamic-live-activity.js +2 -0
- package/build/esm/dynamic-live-activity.js.map +1 -0
- package/build/esm/index.js +6 -2
- package/build/esm/index.js.map +1 -1
- package/build/esm/serverUpdate.js +112 -0
- package/build/esm/serverUpdate.js.map +1 -0
- package/build/esm/utils/packageVersion.js +13 -0
- package/build/esm/utils/packageVersion.js.map +1 -0
- package/build/esm/utils/prerender.js +24 -126
- package/build/esm/utils/prerender.js.map +1 -1
- package/build/esm/widgetServerUpdate.js +59 -0
- package/build/esm/widgetServerUpdate.js.map +1 -0
- package/build/types/constants.d.ts +0 -2
- package/build/types/constants.d.ts.map +1 -1
- package/build/types/dynamic-live-activity.d.ts +2 -0
- package/build/types/dynamic-live-activity.d.ts.map +1 -0
- package/build/types/index.d.ts +11 -4
- package/build/types/index.d.ts.map +1 -1
- package/build/types/serverUpdate.d.ts +80 -0
- package/build/types/serverUpdate.d.ts.map +1 -0
- package/build/types/types.d.ts +22 -0
- package/build/types/types.d.ts.map +1 -1
- package/build/types/utils/packageVersion.d.ts +2 -0
- package/build/types/utils/packageVersion.d.ts.map +1 -0
- package/build/types/utils/prerender.d.ts +22 -8
- package/build/types/utils/prerender.d.ts.map +1 -1
- package/build/types/widgetServerUpdate.d.ts +34 -0
- package/build/types/widgetServerUpdate.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/constants.ts +0 -3
- package/src/dynamic-live-activity.node.test.ts +18 -0
- package/src/dynamic-live-activity.ts +1 -0
- package/src/index.ts +33 -3
- package/src/serverUpdate.node.test.ts +126 -0
- package/src/serverUpdate.ts +160 -0
- package/src/types.ts +25 -0
- package/src/utils/packageVersion.node.test.ts +24 -0
- package/src/utils/packageVersion.ts +19 -0
- package/src/utils/prerender.node.test.ts +2 -1
- package/src/utils/prerender.ts +34 -155
- package/src/widgetServerUpdate.ts +109 -0
package/src/utils/prerender.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
1
|
import path from 'node:path'
|
|
3
|
-
import { createRequire } from 'node:module'
|
|
4
|
-
import vm from 'node:vm'
|
|
5
2
|
|
|
6
|
-
import
|
|
3
|
+
import { createWidgetModuleLoader, type WidgetModuleLoader, type WidgetModulePlatform } from '@use-voltra/compiler'
|
|
7
4
|
|
|
8
|
-
import { MODULE_EXTENSIONS } from '../constants'
|
|
9
5
|
import type { WidgetInitialStatePath, WidgetLabel } from '../types'
|
|
10
6
|
import { logger } from './logger'
|
|
11
7
|
import { isWidgetLocalizedMap } from './widgetLabel'
|
|
@@ -26,170 +22,51 @@ export interface PrerenderableWidget {
|
|
|
26
22
|
/** widgetId -> locale key -> prerendered JSON string (single-file widgets use `__default`) */
|
|
27
23
|
export type PrerenderedWidgetStates = Map<string, Map<string, string>>
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
export interface WidgetModuleEvaluationOptions {
|
|
26
|
+
projectRoot: string
|
|
27
|
+
/** Platform being prebuilt. Determines `Platform.OS` inside widget code. */
|
|
28
|
+
platform: WidgetModulePlatform
|
|
29
|
+
/** Reuse a loader across several widgets so warnings are reported once per prebuild. */
|
|
30
|
+
loader?: WidgetModuleLoader
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve a module path, trying different extensions
|
|
43
|
-
*/
|
|
44
|
-
function resolveModulePath(moduleSpecifier: string, fromDir: string): string | null {
|
|
45
|
-
const basePath = path.resolve(fromDir, moduleSpecifier)
|
|
46
|
-
|
|
47
|
-
for (const ext of MODULE_EXTENSIONS) {
|
|
48
|
-
const fullPath = basePath + ext
|
|
49
|
-
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
|
50
|
-
return fullPath
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Try index files if it's a directory
|
|
55
|
-
if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {
|
|
56
|
-
for (const ext of MODULE_EXTENSIONS) {
|
|
57
|
-
const indexPath = path.join(basePath, 'index' + ext)
|
|
58
|
-
if (fs.existsSync(indexPath)) {
|
|
59
|
-
return indexPath
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getProjectBabelConfigPath(projectRoot: string): string | null {
|
|
68
|
-
const configPath = path.join(projectRoot, 'babel.config.js')
|
|
69
|
-
return fs.existsSync(configPath) ? configPath : null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function getFallbackExpoPreset(projectRoot: string): string {
|
|
73
|
-
return require.resolve('babel-preset-expo', { paths: [projectRoot] })
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Transpile a file with Babel
|
|
34
|
+
* Create the loader used to evaluate widget source during prebuild.
|
|
35
|
+
*
|
|
36
|
+
* Evaluation rules live in `@use-voltra/compiler` so that prebuild, `voltra apply`, and
|
|
37
|
+
* the Metro widget bundler agree on what widget code may import.
|
|
78
38
|
*/
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
? { configFile: projectBabelConfigPath }
|
|
88
|
-
: {
|
|
89
|
-
babelrc: false,
|
|
90
|
-
configFile: false,
|
|
91
|
-
presets: [getFallbackExpoPreset(projectRoot)],
|
|
92
|
-
}),
|
|
39
|
+
export function createPrerenderWidgetModuleLoader(
|
|
40
|
+
projectRoot: string,
|
|
41
|
+
platform: WidgetModulePlatform
|
|
42
|
+
): WidgetModuleLoader {
|
|
43
|
+
return createWidgetModuleLoader({
|
|
44
|
+
projectRoot,
|
|
45
|
+
platform,
|
|
46
|
+
onWarning: (message) => logger.warn(message),
|
|
93
47
|
})
|
|
48
|
+
}
|
|
94
49
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return result.code
|
|
50
|
+
function resolveLoader({ projectRoot, platform, loader }: WidgetModuleEvaluationOptions): WidgetModuleLoader {
|
|
51
|
+
return loader ?? createPrerenderWidgetModuleLoader(projectRoot, platform)
|
|
100
52
|
}
|
|
101
53
|
|
|
102
54
|
/**
|
|
103
|
-
* Evaluate a widget module
|
|
104
|
-
* This allows executing widget code that uses JSX and React components.
|
|
105
|
-
* Local module dependencies are also transpiled with the same Babel settings.
|
|
55
|
+
* Evaluate a widget module and return its exports object.
|
|
106
56
|
*
|
|
107
57
|
* Exported so platform-specific prerender flows can reuse the same module loader rather
|
|
108
|
-
* than duplicating the Babel + VM scaffolding.
|
|
109
|
-
*
|
|
58
|
+
* than duplicating the Babel + VM scaffolding. Callers decide whether to read `.default`,
|
|
59
|
+
* a named export, etc.
|
|
110
60
|
*/
|
|
111
|
-
export function evaluateWidgetModuleExports(
|
|
112
|
-
|
|
113
|
-
filePath: string,
|
|
114
|
-
warnedRedirects = new Set<string>()
|
|
115
|
-
): any {
|
|
116
|
-
// Cache for already-evaluated modules to handle circular dependencies
|
|
117
|
-
const moduleCache = new Map<string, any>()
|
|
118
|
-
const projectRequire = createRequire(path.join(projectRoot, 'package.json'))
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Custom require that transpiles local modules with Babel
|
|
122
|
-
*/
|
|
123
|
-
function customRequire(moduleSpecifier: string, currentDir: string): any {
|
|
124
|
-
// For non-local modules (npm packages), use native require
|
|
125
|
-
if (!isLocalModule(moduleSpecifier)) {
|
|
126
|
-
const redirectedSpecifier = PRERENDER_PACKAGE_REDIRECTS[moduleSpecifier]
|
|
127
|
-
|
|
128
|
-
if (redirectedSpecifier) {
|
|
129
|
-
if (!warnedRedirects.has(moduleSpecifier)) {
|
|
130
|
-
warnedRedirects.add(moduleSpecifier)
|
|
131
|
-
logger.warn(
|
|
132
|
-
`Prerendering initial state imported '${moduleSpecifier}'. Using '${redirectedSpecifier}' instead.`
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return projectRequire(redirectedSpecifier)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return projectRequire(moduleSpecifier)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Resolve the local module path
|
|
143
|
-
const resolvedPath = resolveModulePath(moduleSpecifier, currentDir)
|
|
144
|
-
if (!resolvedPath) {
|
|
145
|
-
throw new Error(`Cannot resolve module '${moduleSpecifier}' from '${currentDir}'`)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Return cached module if already evaluated
|
|
149
|
-
if (moduleCache.has(resolvedPath)) {
|
|
150
|
-
return moduleCache.get(resolvedPath)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Transpile and evaluate the module
|
|
154
|
-
const transpiledCode = transpileFile(resolvedPath, projectRoot)
|
|
155
|
-
const moduleDir = path.dirname(resolvedPath)
|
|
156
|
-
|
|
157
|
-
const mockModule = { exports: {} as any }
|
|
158
|
-
|
|
159
|
-
// Create require function bound to the module's directory
|
|
160
|
-
const boundRequire = (spec: string) => customRequire(spec, moduleDir)
|
|
161
|
-
|
|
162
|
-
const context = vm.createContext({
|
|
163
|
-
exports: mockModule.exports,
|
|
164
|
-
module: mockModule,
|
|
165
|
-
require: boundRequire,
|
|
166
|
-
__filename: resolvedPath,
|
|
167
|
-
__dirname: moduleDir,
|
|
168
|
-
console: console,
|
|
169
|
-
process: process,
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
// Cache before evaluation to handle circular dependencies
|
|
173
|
-
moduleCache.set(resolvedPath, mockModule.exports)
|
|
174
|
-
|
|
175
|
-
const script = new vm.Script(transpiledCode, { filename: resolvedPath })
|
|
176
|
-
script.runInContext(context)
|
|
177
|
-
|
|
178
|
-
// Update cache with final exports (in case module.exports was reassigned)
|
|
179
|
-
moduleCache.set(resolvedPath, mockModule.exports)
|
|
180
|
-
|
|
181
|
-
return mockModule.exports
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return customRequire(filePath, path.dirname(filePath))
|
|
61
|
+
export function evaluateWidgetModuleExports(filePath: string, options: WidgetModuleEvaluationOptions): any {
|
|
62
|
+
return resolveLoader(options).load(filePath)
|
|
185
63
|
}
|
|
186
64
|
|
|
187
65
|
/**
|
|
188
66
|
* Evaluate a widget file as a server-style WidgetVariants module and return its object export.
|
|
189
67
|
*/
|
|
190
|
-
export function evaluateWidgetModule(
|
|
191
|
-
const
|
|
192
|
-
const widgetVariants: any = exports.default || exports
|
|
68
|
+
export function evaluateWidgetModule(filePath: string, options: WidgetModuleEvaluationOptions): any {
|
|
69
|
+
const widgetVariants = resolveLoader(options).loadDefaultExport(filePath)
|
|
193
70
|
|
|
194
71
|
if (!widgetVariants || typeof widgetVariants !== 'object') {
|
|
195
72
|
throw new Error('Widget file must export a WidgetVariants object or have a default export of WidgetVariants')
|
|
@@ -208,15 +85,17 @@ export function evaluateWidgetModule(projectRoot: string, filePath: string, warn
|
|
|
208
85
|
* @param widgets - Array of widget configurations
|
|
209
86
|
* @param projectRoot - Root directory of the Expo project
|
|
210
87
|
* @param renderer - The renderer function to use (voltra/server or voltra/android/server)
|
|
88
|
+
* @param platform - Platform being prebuilt
|
|
211
89
|
* @returns Map of widgetId -> (locale key -> prerendered JSON string)
|
|
212
90
|
*/
|
|
213
91
|
export async function prerenderWidgetState(
|
|
214
92
|
widgets: PrerenderableWidget[],
|
|
215
93
|
projectRoot: string,
|
|
216
|
-
renderer: WidgetRenderer
|
|
94
|
+
renderer: WidgetRenderer,
|
|
95
|
+
platform: WidgetModulePlatform
|
|
217
96
|
): Promise<PrerenderedWidgetStates> {
|
|
218
97
|
const prerenderedStates: PrerenderedWidgetStates = new Map()
|
|
219
|
-
const
|
|
98
|
+
const loader = createPrerenderWidgetModuleLoader(projectRoot, platform)
|
|
220
99
|
|
|
221
100
|
for (const widget of widgets) {
|
|
222
101
|
if (!widget.initialStatePath) {
|
|
@@ -233,7 +112,7 @@ export async function prerenderWidgetState(
|
|
|
233
112
|
try {
|
|
234
113
|
for (const [localeKey, relativePath] of Object.entries(perLocalePaths)) {
|
|
235
114
|
const absoluteWidgetPath = path.resolve(projectRoot, relativePath)
|
|
236
|
-
const widgetVariants = evaluateWidgetModule(projectRoot,
|
|
115
|
+
const widgetVariants = evaluateWidgetModule(absoluteWidgetPath, { projectRoot, platform, loader })
|
|
237
116
|
const prerenderedState = renderer(widgetVariants)
|
|
238
117
|
inner.set(localeKey, prerenderedState)
|
|
239
118
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { resolveServerUpdateInterval, resolveServerUpdateUrl, validateServerUpdateRefresh } from './serverUpdate'
|
|
2
|
+
import { logger } from './utils/logger'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Expo-plugin side of the `serverUpdate` config rules. The rules themselves live in
|
|
6
|
+
* `./serverUpdate`, which the `voltra` CLI mirrors; this module only turns them into the plugin's
|
|
7
|
+
* thrown errors and console warnings.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** `serverUpdate` as it appears in app.json, before defaults are applied. */
|
|
11
|
+
export interface WidgetServerUpdateConfig {
|
|
12
|
+
url?: string
|
|
13
|
+
intervalMinutes?: number
|
|
14
|
+
refresh?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `serverUpdate` after defaults, as the generators consume it. */
|
|
18
|
+
export interface ResolvedWidgetServerUpdateConfig {
|
|
19
|
+
/** Absent when app.json set no URL — the app supplies one at runtime. */
|
|
20
|
+
url?: string
|
|
21
|
+
intervalMinutes: number
|
|
22
|
+
refresh: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WidgetServerUpdateRules {
|
|
26
|
+
/** True when the widget has an `entry` and so renders bundled JS from fetched props. */
|
|
27
|
+
hasEntry: boolean
|
|
28
|
+
/** Interval used when the widget sets none. Ignored for widgets with `entry`. */
|
|
29
|
+
defaultIntervalMinutes: number
|
|
30
|
+
/** Platform floor for payload widgets. Ignored for widgets with `entry`. */
|
|
31
|
+
minimumIntervalMinutes: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validateWidgetServerUpdate(
|
|
35
|
+
serverUpdate: unknown,
|
|
36
|
+
widgetId: string,
|
|
37
|
+
rules: WidgetServerUpdateRules
|
|
38
|
+
): void {
|
|
39
|
+
if (serverUpdate === undefined) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const context = `Widget '${widgetId}': serverUpdate`
|
|
44
|
+
|
|
45
|
+
if (typeof serverUpdate !== 'object' || serverUpdate === null || Array.isArray(serverUpdate)) {
|
|
46
|
+
throw new Error(`${context} must be an object`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { url, intervalMinutes, refresh } = serverUpdate as WidgetServerUpdateConfig
|
|
50
|
+
|
|
51
|
+
const resolvedUrl = resolveServerUpdateUrl(url, context)
|
|
52
|
+
|
|
53
|
+
if (resolvedUrl.kind === 'invalid') {
|
|
54
|
+
throw new Error(resolvedUrl.error)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (resolvedUrl.kind === 'insecure') {
|
|
58
|
+
logger.warn(resolvedUrl.warning)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const interval = resolveServerUpdateInterval({
|
|
62
|
+
intervalMinutes,
|
|
63
|
+
context,
|
|
64
|
+
hasEntry: rules.hasEntry,
|
|
65
|
+
defaultIntervalMinutes: rules.defaultIntervalMinutes,
|
|
66
|
+
minimumIntervalMinutes: rules.minimumIntervalMinutes,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (interval.kind === 'invalid') {
|
|
70
|
+
throw new Error(interval.error)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (interval.kind === 'clamped') {
|
|
74
|
+
logger.warn(interval.warning)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const refreshError = validateServerUpdateRefresh(refresh, context)
|
|
78
|
+
|
|
79
|
+
if (refreshError) {
|
|
80
|
+
throw new Error(refreshError)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Applies defaults to a validated `serverUpdate`. Generators call this instead of reading
|
|
86
|
+
* `intervalMinutes` directly, so the value written into a plist or a generated asset is the one
|
|
87
|
+
* the widget will actually be scheduled on.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveWidgetServerUpdate(
|
|
90
|
+
serverUpdate: WidgetServerUpdateConfig,
|
|
91
|
+
rules: WidgetServerUpdateRules
|
|
92
|
+
): ResolvedWidgetServerUpdateConfig {
|
|
93
|
+
const interval = resolveServerUpdateInterval({
|
|
94
|
+
intervalMinutes: serverUpdate.intervalMinutes,
|
|
95
|
+
context: 'serverUpdate',
|
|
96
|
+
hasEntry: rules.hasEntry,
|
|
97
|
+
defaultIntervalMinutes: rules.defaultIntervalMinutes,
|
|
98
|
+
minimumIntervalMinutes: rules.minimumIntervalMinutes,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
url: serverUpdate.url,
|
|
103
|
+
// An invalid interval cannot reach here — validateWidgetServerUpdate throws on it first — but
|
|
104
|
+
// if the two are ever called out of order, falling back to the platform's own default is less
|
|
105
|
+
// surprising than silently switching a payload widget to the Dynamic one.
|
|
106
|
+
intervalMinutes: interval.kind === 'invalid' ? rules.defaultIntervalMinutes : interval.intervalMinutes,
|
|
107
|
+
refresh: serverUpdate.refresh === true,
|
|
108
|
+
}
|
|
109
|
+
}
|