expo-widgets 56.0.9 → 56.0.11
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/CHANGELOG.md +10 -0
- package/package.json +6 -6
- package/plugin/build/withWidgetSourceFiles.js +73 -33
- package/plugin/src/withWidgetSourceFiles.ts +54 -11
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 56.0.11 — 2026-05-20
|
|
14
|
+
|
|
15
|
+
_This version does not introduce any user-facing changes._
|
|
16
|
+
|
|
17
|
+
## 56.0.10 — 2026-05-19
|
|
18
|
+
|
|
19
|
+
### 💡 Others
|
|
20
|
+
|
|
21
|
+
- Validate/sanitize config-plugin inputs more comprehensively ([#45884](https://github.com/expo/expo/pull/45884) by [@kitten](https://github.com/kitten))
|
|
22
|
+
|
|
13
23
|
## 56.0.9 — 2026-05-15
|
|
14
24
|
|
|
15
25
|
### 🎉 New features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-widgets",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.11",
|
|
4
4
|
"description": "Widgets.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -21,16 +21,16 @@
|
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"homepage": "https://docs.expo.dev/versions/latest/sdk/widgets/",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@expo/plist": "^0.
|
|
25
|
-
"@expo/ui": "~56.0.
|
|
24
|
+
"@expo/plist": "^0.7.0",
|
|
25
|
+
"@expo/ui": "~56.0.10"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@expo/spawn-async": "^1.
|
|
28
|
+
"@expo/spawn-async": "^1.8.0",
|
|
29
29
|
"@types/node": "^22.14.0",
|
|
30
30
|
"@types/react": "~19.2.0",
|
|
31
31
|
"react-native": "0.85.3",
|
|
32
32
|
"resolve-workspace-root": "^2.0.0",
|
|
33
|
-
"expo": "56.0.0
|
|
33
|
+
"expo": "56.0.0",
|
|
34
34
|
"expo-module-scripts": "56.0.2"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"./scripts/xcode-build-bundle.sh"
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "c4c9867a0bcbb188e55ecaec4998e38d33108a5d",
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "expo-module build",
|
|
50
50
|
"build:plugin": "expo-module build plugin",
|
|
@@ -40,31 +40,71 @@ const plist_1 = __importDefault(require("@expo/plist"));
|
|
|
40
40
|
const config_plugins_1 = require("expo/config-plugins");
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
const WidgetFamily_type_1 = require("./types/WidgetFamily.type");
|
|
44
|
+
const VALID_WIDGET_FAMILIES = new Set(Object.values(WidgetFamily_type_1.WidgetFamily));
|
|
45
|
+
function assertSwiftIdentifier(value, label) {
|
|
46
|
+
if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
47
|
+
throw new Error(`Invalid ${label} ${JSON.stringify(value)}: must be a Swift identifier.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function assertWidgetFamily(value) {
|
|
51
|
+
if (typeof value !== 'string' || !VALID_WIDGET_FAMILIES.has(value)) {
|
|
52
|
+
throw new Error(`Invalid supportedFamilies entry ${JSON.stringify(value)}: must be one of ${[...VALID_WIDGET_FAMILIES].join(', ')}.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function validateWidget(widget) {
|
|
56
|
+
assertSwiftIdentifier(widget.name, 'widget name');
|
|
57
|
+
for (const family of widget.supportedFamilies) {
|
|
58
|
+
assertWidgetFamily(family);
|
|
59
|
+
}
|
|
60
|
+
if (widget.configuration) {
|
|
61
|
+
for (const [paramName, param] of Object.entries(widget.configuration.parameters)) {
|
|
62
|
+
assertSwiftIdentifier(paramName, 'parameter name');
|
|
63
|
+
if (param.type === 'number' && typeof param.default !== 'number') {
|
|
64
|
+
throw new Error(`Invalid default for ${JSON.stringify(paramName)}: must be a number.`);
|
|
65
|
+
}
|
|
66
|
+
else if (param.type === 'boolean' && typeof param.default !== 'boolean') {
|
|
67
|
+
throw new Error(`Invalid default for ${JSON.stringify(paramName)}: must be a boolean.`);
|
|
68
|
+
}
|
|
69
|
+
else if (param.type === 'enum') {
|
|
70
|
+
assertSwiftIdentifier(param.default, `default for ${JSON.stringify(paramName)}`);
|
|
71
|
+
for (const value of param.values) {
|
|
72
|
+
assertSwiftIdentifier(value.value, `enum case for ${JSON.stringify(paramName)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
54
75
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const withWidgetSourceFiles = (config, { widgets, targetName, onFilesGenerated, groupIdentifier }) => {
|
|
79
|
+
for (const widget of widgets) {
|
|
80
|
+
validateWidget(widget);
|
|
81
|
+
}
|
|
82
|
+
return (0, config_plugins_1.withDangerousMod)(config, [
|
|
83
|
+
'ios',
|
|
84
|
+
async (config) => {
|
|
85
|
+
const projectRoot = config.modRequest.platformProjectRoot;
|
|
86
|
+
const targetDirectory = path.join(projectRoot, targetName);
|
|
87
|
+
const existingInfoPlistPath = path.join(targetDirectory, 'Info.plist');
|
|
88
|
+
const existingInfoPlist = fs.existsSync(existingInfoPlistPath)
|
|
89
|
+
? fs.readFileSync(existingInfoPlistPath, 'utf8')
|
|
90
|
+
: null;
|
|
91
|
+
if (fs.existsSync(targetDirectory)) {
|
|
92
|
+
fs.rmSync(targetDirectory, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
95
|
+
const entitlementsPath = path.join(targetDirectory, `${targetName}.entitlements`);
|
|
96
|
+
const entitlementsContent = {
|
|
97
|
+
'com.apple.security.application-groups': [groupIdentifier],
|
|
98
|
+
};
|
|
99
|
+
fs.writeFileSync(entitlementsPath, plist_1.default.build(entitlementsContent));
|
|
100
|
+
const infoPlistPath = createInfoPlist(groupIdentifier, targetDirectory, config.ios?.version ?? config.version ?? '1.0', config.ios?.buildNumber ?? '1', existingInfoPlist);
|
|
101
|
+
const indexSwiftPath = createIndexSwift(widgets, targetDirectory);
|
|
102
|
+
const widgetSwiftPaths = widgets.map((widget) => createWidgetSwift(widget, targetDirectory));
|
|
103
|
+
onFilesGenerated([entitlementsPath, infoPlistPath, indexSwiftPath, ...widgetSwiftPaths]);
|
|
104
|
+
return config;
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
};
|
|
68
108
|
const createIndexSwift = (widgets, targetPath) => {
|
|
69
109
|
const indexFilePath = path.join(targetPath, `index.swift`);
|
|
70
110
|
const numberOfWidgets = widgets.length;
|
|
@@ -138,8 +178,8 @@ struct ${widget.name}: Widget {
|
|
|
138
178
|
StaticConfiguration(kind: name, provider: WidgetsTimelineProvider(name: name)) { entry in
|
|
139
179
|
WidgetsEntryView(entry: entry)
|
|
140
180
|
}
|
|
141
|
-
.configurationDisplayName(
|
|
142
|
-
.description(
|
|
181
|
+
.configurationDisplayName(${JSON.stringify(widget.displayName)})
|
|
182
|
+
.description(${JSON.stringify(widget.description)})
|
|
143
183
|
.supportedFamilies([.${widget.supportedFamilies.join(', .')}])${widget.contentMarginsDisabled ? '\n .contentMarginsDisabled()' : ''}
|
|
144
184
|
}
|
|
145
185
|
}`;
|
|
@@ -150,8 +190,8 @@ internal import ExpoWidgets
|
|
|
150
190
|
|
|
151
191
|
// AppIntent
|
|
152
192
|
struct ${widget.name}ConfigurationAppIntent: WidgetConfigurationIntent {
|
|
153
|
-
static var title: LocalizedStringResource =
|
|
154
|
-
${widget.configuration?.description ? ` static var description: LocalizedStringResource =
|
|
193
|
+
static var title: LocalizedStringResource = ${JSON.stringify(`${widget.configuration?.title ?? widget.displayName} Configuration`)}
|
|
194
|
+
${widget.configuration?.description ? ` static var description: LocalizedStringResource = ${JSON.stringify(widget.configuration.description)}\n` : ''}
|
|
155
195
|
${Object.entries(widget.configuration?.parameters ?? {})
|
|
156
196
|
.map(([name, param]) => {
|
|
157
197
|
let paramType;
|
|
@@ -171,7 +211,7 @@ ${Object.entries(widget.configuration?.parameters ?? {})
|
|
|
171
211
|
default:
|
|
172
212
|
paramType = 'String';
|
|
173
213
|
}
|
|
174
|
-
return ` @Parameter(title:
|
|
214
|
+
return ` @Parameter(title: ${JSON.stringify(param.title)}, default: ${param.type === 'string' ? JSON.stringify(param.default) : param.type === 'number' ? param.default : param.type === 'boolean' ? param.default : `${widget.name}${name[0]?.toUpperCase() + name.slice(1)}Enum.${param.default}`})\n var ${name}: ${paramType}`;
|
|
175
215
|
})
|
|
176
216
|
.join('\n')}
|
|
177
217
|
|
|
@@ -192,12 +232,12 @@ enum ${paramTypeName}: String, CaseIterable, AppEnum {
|
|
|
192
232
|
})
|
|
193
233
|
.join('\n ')}
|
|
194
234
|
|
|
195
|
-
static var typeDisplayRepresentation = TypeDisplayRepresentation(name:
|
|
235
|
+
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: ${JSON.stringify(param.title)})
|
|
196
236
|
|
|
197
237
|
static var caseDisplayRepresentations: [${paramTypeName}: DisplayRepresentation] = [
|
|
198
238
|
${param.values
|
|
199
239
|
.map((value) => {
|
|
200
|
-
return `.${value.value}: DisplayRepresentation(title:
|
|
240
|
+
return `.${value.value}: DisplayRepresentation(title: ${JSON.stringify(value.name)})`;
|
|
201
241
|
})
|
|
202
242
|
.join(',\n ')}
|
|
203
243
|
]
|
|
@@ -297,8 +337,8 @@ struct ${widget.name}: Widget {
|
|
|
297
337
|
return AppIntentConfiguration(kind: name, intent: ${widget.name}ConfigurationAppIntent.self, provider: ${widget.name}TimelineProvider()) { entry in
|
|
298
338
|
${widget.name}EntryView(entry: entry)
|
|
299
339
|
}
|
|
300
|
-
.configurationDisplayName(
|
|
301
|
-
.description(
|
|
340
|
+
.configurationDisplayName(${JSON.stringify(widget.displayName)})
|
|
341
|
+
.description(${JSON.stringify(widget.description)})
|
|
302
342
|
.supportedFamilies([.${widget.supportedFamilies.join(', .')}])${widget.contentMarginsDisabled ? '\n .contentMarginsDisabled()' : ''}
|
|
303
343
|
}
|
|
304
344
|
}`;
|
|
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
|
|
6
6
|
import { WidgetConfig } from './types/WidgetConfig.type';
|
|
7
|
+
import { WidgetFamily } from './types/WidgetFamily.type';
|
|
7
8
|
|
|
8
9
|
type WidgetSourceFilesProps = {
|
|
9
10
|
targetName: string;
|
|
@@ -12,11 +13,52 @@ type WidgetSourceFilesProps = {
|
|
|
12
13
|
onFilesGenerated: (files: string[]) => void;
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
const VALID_WIDGET_FAMILIES: ReadonlySet<string> = new Set(Object.values(WidgetFamily));
|
|
17
|
+
|
|
18
|
+
function assertSwiftIdentifier(value: unknown, label: string): asserts value is string {
|
|
19
|
+
if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
20
|
+
throw new Error(`Invalid ${label} ${JSON.stringify(value)}: must be a Swift identifier.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertWidgetFamily(value: unknown): asserts value is WidgetFamily {
|
|
25
|
+
if (typeof value !== 'string' || !VALID_WIDGET_FAMILIES.has(value)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Invalid supportedFamilies entry ${JSON.stringify(value)}: must be one of ${[...VALID_WIDGET_FAMILIES].join(', ')}.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateWidget(widget: WidgetConfig): void {
|
|
33
|
+
assertSwiftIdentifier(widget.name, 'widget name');
|
|
34
|
+
for (const family of widget.supportedFamilies) {
|
|
35
|
+
assertWidgetFamily(family);
|
|
36
|
+
}
|
|
37
|
+
if (widget.configuration) {
|
|
38
|
+
for (const [paramName, param] of Object.entries(widget.configuration.parameters)) {
|
|
39
|
+
assertSwiftIdentifier(paramName, 'parameter name');
|
|
40
|
+
if (param.type === 'number' && typeof param.default !== 'number') {
|
|
41
|
+
throw new Error(`Invalid default for ${JSON.stringify(paramName)}: must be a number.`);
|
|
42
|
+
} else if (param.type === 'boolean' && typeof param.default !== 'boolean') {
|
|
43
|
+
throw new Error(`Invalid default for ${JSON.stringify(paramName)}: must be a boolean.`);
|
|
44
|
+
} else if (param.type === 'enum') {
|
|
45
|
+
assertSwiftIdentifier(param.default, `default for ${JSON.stringify(paramName)}`);
|
|
46
|
+
for (const value of param.values) {
|
|
47
|
+
assertSwiftIdentifier(value.value, `enum case for ${JSON.stringify(paramName)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
const withWidgetSourceFiles: ConfigPlugin<WidgetSourceFilesProps> = (
|
|
16
55
|
config,
|
|
17
56
|
{ widgets, targetName, onFilesGenerated, groupIdentifier }
|
|
18
|
-
) =>
|
|
19
|
-
|
|
57
|
+
) => {
|
|
58
|
+
for (const widget of widgets) {
|
|
59
|
+
validateWidget(widget);
|
|
60
|
+
}
|
|
61
|
+
return withDangerousMod(config, [
|
|
20
62
|
'ios',
|
|
21
63
|
async (config) => {
|
|
22
64
|
const projectRoot = config.modRequest.platformProjectRoot;
|
|
@@ -50,6 +92,7 @@ const withWidgetSourceFiles: ConfigPlugin<WidgetSourceFilesProps> = (
|
|
|
50
92
|
return config;
|
|
51
93
|
},
|
|
52
94
|
]);
|
|
95
|
+
};
|
|
53
96
|
|
|
54
97
|
const createIndexSwift = (widgets: WidgetConfig[], targetPath: string): string => {
|
|
55
98
|
const indexFilePath = path.join(targetPath, `index.swift`);
|
|
@@ -143,8 +186,8 @@ struct ${widget.name}: Widget {
|
|
|
143
186
|
StaticConfiguration(kind: name, provider: WidgetsTimelineProvider(name: name)) { entry in
|
|
144
187
|
WidgetsEntryView(entry: entry)
|
|
145
188
|
}
|
|
146
|
-
.configurationDisplayName(
|
|
147
|
-
.description(
|
|
189
|
+
.configurationDisplayName(${JSON.stringify(widget.displayName)})
|
|
190
|
+
.description(${JSON.stringify(widget.description)})
|
|
148
191
|
.supportedFamilies([.${widget.supportedFamilies.join(', .')}])${widget.contentMarginsDisabled ? '\n .contentMarginsDisabled()' : ''}
|
|
149
192
|
}
|
|
150
193
|
}`;
|
|
@@ -155,8 +198,8 @@ internal import ExpoWidgets
|
|
|
155
198
|
|
|
156
199
|
// AppIntent
|
|
157
200
|
struct ${widget.name}ConfigurationAppIntent: WidgetConfigurationIntent {
|
|
158
|
-
static var title: LocalizedStringResource =
|
|
159
|
-
${widget.configuration?.description ? ` static var description: LocalizedStringResource =
|
|
201
|
+
static var title: LocalizedStringResource = ${JSON.stringify(`${widget.configuration?.title ?? widget.displayName} Configuration`)}
|
|
202
|
+
${widget.configuration?.description ? ` static var description: LocalizedStringResource = ${JSON.stringify(widget.configuration.description)}\n` : ''}
|
|
160
203
|
${Object.entries(widget.configuration?.parameters ?? {})
|
|
161
204
|
.map(([name, param]) => {
|
|
162
205
|
let paramType: string;
|
|
@@ -176,7 +219,7 @@ ${Object.entries(widget.configuration?.parameters ?? {})
|
|
|
176
219
|
default:
|
|
177
220
|
paramType = 'String';
|
|
178
221
|
}
|
|
179
|
-
return ` @Parameter(title:
|
|
222
|
+
return ` @Parameter(title: ${JSON.stringify(param.title)}, default: ${param.type === 'string' ? JSON.stringify(param.default) : param.type === 'number' ? param.default : param.type === 'boolean' ? param.default : `${widget.name}${name[0]?.toUpperCase() + name.slice(1)}Enum.${param.default}`})\n var ${name}: ${paramType}`;
|
|
180
223
|
})
|
|
181
224
|
.join('\n')}
|
|
182
225
|
|
|
@@ -196,12 +239,12 @@ enum ${paramTypeName}: String, CaseIterable, AppEnum {
|
|
|
196
239
|
})
|
|
197
240
|
.join('\n ')}
|
|
198
241
|
|
|
199
|
-
static var typeDisplayRepresentation = TypeDisplayRepresentation(name:
|
|
242
|
+
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: ${JSON.stringify(param.title)})
|
|
200
243
|
|
|
201
244
|
static var caseDisplayRepresentations: [${paramTypeName}: DisplayRepresentation] = [
|
|
202
245
|
${param.values
|
|
203
246
|
.map((value) => {
|
|
204
|
-
return `.${value.value}: DisplayRepresentation(title:
|
|
247
|
+
return `.${value.value}: DisplayRepresentation(title: ${JSON.stringify(value.name)})`;
|
|
205
248
|
})
|
|
206
249
|
.join(',\n ')}
|
|
207
250
|
]
|
|
@@ -301,8 +344,8 @@ struct ${widget.name}: Widget {
|
|
|
301
344
|
return AppIntentConfiguration(kind: name, intent: ${widget.name}ConfigurationAppIntent.self, provider: ${widget.name}TimelineProvider()) { entry in
|
|
302
345
|
${widget.name}EntryView(entry: entry)
|
|
303
346
|
}
|
|
304
|
-
.configurationDisplayName(
|
|
305
|
-
.description(
|
|
347
|
+
.configurationDisplayName(${JSON.stringify(widget.displayName)})
|
|
348
|
+
.description(${JSON.stringify(widget.description)})
|
|
306
349
|
.supportedFamilies([.${widget.supportedFamilies.join(', .')}])${widget.contentMarginsDisabled ? '\n .contentMarginsDisabled()' : ''}
|
|
307
350
|
}
|
|
308
351
|
}`;
|