catalyst-core 0.2.0-beta.3 → 0.3.0-beta.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/babel.native.config.cjs +8 -0
- package/bin/catalyst.js +49 -105
- package/changelog.md +8 -0
- package/dist/caching.js +1 -1
- package/dist/index.jsx +8 -0
- package/dist/logger.js +74 -3
- package/dist/native/androidSetup.js +300 -8
- package/dist/native/bridge/WebBridge.js +481 -59
- package/dist/native/bridge/constants/NativeInterfaces.js +290 -45
- package/dist/native/bridge/errors.js +250 -26
- package/dist/native/bridge/hooks/index.js +125 -1
- package/dist/native/bridge/hooks/useCamera.js +347 -9
- package/dist/native/bridge/hooks/useDataProtection.js +200 -2
- package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
- package/dist/native/bridge/hooks/useFilePicker.js +429 -2
- package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
- package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
- package/dist/native/bridge/hooks/useIntent.js +95 -1
- package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
- package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
- package/dist/native/bridge/hooks/useNotification.js +318 -2
- package/dist/native/bridge/hooks/useSafeArea.js +84 -2
- package/dist/native/bridge/hooks/useVideoStream.js +434 -6
- package/dist/native/bridge/hooks/utils.js +27 -1
- package/dist/native/bridge/hooks.js +16 -1
- package/dist/native/bridge/perf/collectors/ApiCollector.js +66 -3
- package/dist/native/bridge/perf/collectors/CacheCollector.js +186 -11
- package/dist/native/bridge/perf/collectors/HookCollector.js +141 -19
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +481 -23
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +245 -10
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +107 -8
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +273 -32
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +178 -2
- package/dist/native/bridge/perf/collectors/RenderCollector.js +258 -14
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +95 -9
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +90 -6
- package/dist/native/bridge/perf/core/constants.js +103 -20
- package/dist/native/bridge/perf/core/measure.js +179 -30
- package/dist/native/bridge/perf/core/session.js +89 -23
- package/dist/native/bridge/perf/core/store.js +554 -1
- package/dist/native/bridge/perf/core/summary.js +174 -1
- package/dist/native/bridge/perf/core/traceExport.js +240 -1
- package/dist/native/bridge/perf/core/waterfalls.js +146 -1
- package/dist/native/bridge/perf/index.js +359 -38
- package/dist/native/bridge/safeArea.js +53 -5
- package/dist/native/bridge/standardizedHooks.js +595 -82
- package/dist/native/bridge/useBaseHook.js +274 -38
- package/dist/native/bridge/utils/CameraUtils.js +201 -56
- package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
- package/dist/native/bridge/utils/NativeBridge.js +425 -141
- package/dist/native/buildAndroid/assets.js +510 -13
- package/dist/native/buildAndroid/build.js +296 -19
- package/dist/native/buildAndroid/config.js +87 -1
- package/dist/native/buildAndroid/index.js +374 -1
- package/dist/native/buildAppAndroid.js +34 -2
- package/dist/native/buildAppIos.js +62 -2
- package/dist/native/buildIos/assets.js +609 -8
- package/dist/native/buildIos/build.js +639 -20
- package/dist/native/buildIos/config.js +294 -12
- package/dist/native/buildIos/index.js +343 -6
- package/dist/native/buildIos/plugins.js +343 -16
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
- package/dist/native/internalPluginUtils.js +232 -7
- package/dist/native/package.json +3 -0
- package/dist/native/plugin-bridge/PluginBridge.js +227 -1
- package/dist/native/pluginComposerAndroid.js +407 -23
- package/dist/native/pluginComposerIos.js +435 -11
- package/dist/native/renameAndroidProject.js +1094 -88
- package/dist/native/setupEmulatorIos.js +289 -22
- package/dist/native/setupServer.js +160 -19
- package/dist/native/terminalProgress.js +199 -11
- package/dist/native/utils.js +193 -15
- package/dist/offline/catalyst-sw.js +145 -1
- package/dist/offline/registerServiceWorker.js +33 -1
- package/dist/otel.js +375 -46
- package/dist/router/ClientRouter.js +14 -2
- package/dist/router/ServerRouter.js +9 -1
- package/dist/router.js +1 -1
- package/dist/scripts/build.js +112 -3
- package/dist/scripts/checkVersion.js +3 -1
- package/dist/scripts/cleanCache.js +30 -1
- package/dist/scripts/generateOfflineManifest.js +116 -1
- package/dist/scripts/loadEnvironmentVariables.js +32 -6
- package/dist/scripts/plugins.js +309 -1
- package/dist/scripts/preServerInit.js +13 -0
- package/dist/scripts/scriptUtils.js +54 -3
- package/dist/scripts/serve.js +51 -4
- package/dist/scripts/start.js +47 -10
- package/dist/scripts/validator.js +149 -1
- package/dist/sentry.cjs +127 -0
- package/dist/sentry.js +125 -5
- package/dist/server/expressServer.js +250 -5
- package/dist/server/manifestCache.js +36 -0
- package/dist/server/renderer/ChunkExtractor.js +144 -0
- package/dist/server/renderer/document/Body.jsx +59 -0
- package/dist/server/renderer/document/FastRefresh.jsx +20 -0
- package/dist/server/renderer/document/Head.jsx +60 -0
- package/dist/server/renderer/document/index.jsx +2 -0
- package/dist/server/renderer/extract.js +149 -31
- package/dist/server/renderer/handler.jsx +433 -0
- package/dist/server/renderer/index.js +8 -1
- package/dist/server/renderer/render.js +47 -0
- package/dist/server/utils/botDetectionMiddleware.js +29 -0
- package/dist/server/utils/cjsRequire.js +10 -0
- package/dist/server/utils/userAgentUtil.js +87 -9
- package/dist/server/utils/validator.js +164 -1
- package/dist/vite/FastRefresh.jsx +20 -0
- package/dist/vite/inject-cache-key-plugin.js +276 -0
- package/dist/vite/loadCustomViteConfig.js +22 -0
- package/dist/vite/manifest-categorization-plugin.js +455 -0
- package/dist/vite/node-loader.mjs +458 -0
- package/dist/vite/scssParams.js +21 -0
- package/dist/vite/vite.config.client.js +130 -0
- package/dist/vite/vite.config.js +422 -0
- package/dist/vite/vite.config.server.js +84 -0
- package/dist/web-router/components/MetaTag.jsx +34 -0
- package/dist/web-router/components/RouterDataProvider.jsx +332 -0
- package/dist/web-router/components/Split.jsx +200 -0
- package/dist/web-router/components/SplitInview.jsx +106 -0
- package/dist/web-router/components/SsrRequestContext.jsx +14 -0
- package/dist/web-router/context.jsx +12 -0
- package/dist/web-router/hooks.jsx +72 -0
- package/dist/web-router/types/components/MetaTag.d.ts +2 -0
- package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
- package/dist/web-router/types/context.d.ts +14 -0
- package/dist/web-router/types/hooks.d.ts +1 -0
- package/dist/web-router/types/index.d.ts +3 -0
- package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
- package/dist/web-router/utils/metaDataUtils.jsx +119 -0
- package/mcp_v2/conversion-tasks.json +35 -17
- package/mcp_v2/knowledge-base.json +124 -13
- package/mcp_v2/lib/helpers.js +10 -0
- package/mcp_v2/mcp.js +4 -4
- package/mcp_v2/package.json +1 -0
- package/mcp_v2/tools/build.js +116 -20
- package/mcp_v2/tools/conversion.js +106 -5
- package/mcp_v2/tools/tasks.js +18 -14
- package/package.json +35 -68
- package/babel.config.js +0 -32
- package/dist/index.js +0 -1
- package/dist/scripts/devBuild.js +0 -3
- package/dist/scripts/devServe.js +0 -3
- package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
- package/dist/scripts/registerAliases.js +0 -6
- package/dist/server/renderer/document/Body.js +0 -11
- package/dist/server/renderer/document/Head.js +0 -9
- package/dist/server/renderer/document/index.js +0 -1
- package/dist/server/renderer/handler.js +0 -47
- package/dist/server/startServer.js +0 -57
- package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
- package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
- package/dist/webpack/babel.config.client.js +0 -1
- package/dist/webpack/babel.config.ssr.js +0 -1
- package/dist/webpack/base.babel.js +0 -7
- package/dist/webpack/development.client.babel.js +0 -10
- package/dist/webpack/production.client.babel.js +0 -4
- package/dist/webpack/production.ssr.babel.js +0 -8
- package/dist/webpack/scssParams.js +0 -9
|
@@ -1,16 +1,440 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
discoverInternalPlugins
|
|
7
|
+
} = require("./internalPluginUtils.js");
|
|
8
|
+
function isDir(dirPath) {
|
|
9
|
+
return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
|
|
10
|
+
}
|
|
11
|
+
function ensureDir(dirPath) {
|
|
12
|
+
if (!fs.existsSync(dirPath)) {
|
|
13
|
+
fs.mkdirSync(dirPath, {
|
|
14
|
+
recursive: true
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function sanitizeForPath(value) {
|
|
19
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
20
|
+
}
|
|
21
|
+
function mustBeNonEmptyString(value, fieldName, sourcePath) {
|
|
22
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
23
|
+
throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);
|
|
24
|
+
}
|
|
25
|
+
return value.trim();
|
|
26
|
+
}
|
|
27
|
+
function asUniqueSorted(values) {
|
|
28
|
+
return [...new Set(values)].sort();
|
|
29
|
+
}
|
|
30
|
+
function isPlainObject(value) {
|
|
31
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
function deepEqual(left, right) {
|
|
34
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
35
|
+
}
|
|
36
|
+
function mergeStructuredValues(existing, incoming, fieldName) {
|
|
37
|
+
if (existing === undefined) {
|
|
38
|
+
return JSON.parse(JSON.stringify(incoming));
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(existing) && Array.isArray(incoming)) {
|
|
41
|
+
const merged = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const value of [...existing, ...incoming]) {
|
|
44
|
+
const key = JSON.stringify(value);
|
|
45
|
+
if (seen.has(key)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
seen.add(key);
|
|
49
|
+
merged.push(value);
|
|
50
|
+
}
|
|
51
|
+
return merged;
|
|
52
|
+
}
|
|
53
|
+
if (isPlainObject(existing) && isPlainObject(incoming)) {
|
|
54
|
+
const merged = {
|
|
55
|
+
...existing
|
|
56
|
+
};
|
|
57
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
58
|
+
merged[key] = mergeStructuredValues(merged[key], value, `${fieldName}.${key}`);
|
|
59
|
+
}
|
|
60
|
+
return merged;
|
|
61
|
+
}
|
|
62
|
+
if (deepEqual(existing, incoming)) {
|
|
63
|
+
return existing;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Conflicting values for '${fieldName}' while composing selected iOS plugins`);
|
|
66
|
+
}
|
|
67
|
+
function dependencyKey(dependency) {
|
|
68
|
+
return dependency.url;
|
|
69
|
+
}
|
|
70
|
+
function requirementKey(dependency) {
|
|
71
|
+
return `${dependency.requirement.type}:${dependency.requirement.version}`;
|
|
72
|
+
}
|
|
73
|
+
function packageKey(dependency) {
|
|
74
|
+
return dependency.package;
|
|
75
|
+
}
|
|
76
|
+
function resolveManifestPath(pluginDir, relativePath, fieldName) {
|
|
77
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Containment and realpath checks are enforced immediately below.
|
|
78
|
+
const resolvedPath = path.resolve(pluginDir, relativePath);
|
|
79
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir comes from the internal plugin root.
|
|
80
|
+
const normalizedPluginDir = fs.realpathSync(path.resolve(pluginDir));
|
|
81
|
+
if (resolvedPath !== normalizedPluginDir && !resolvedPath.startsWith(`${normalizedPluginDir}${path.sep}`)) {
|
|
82
|
+
throw new Error(`'${fieldName}' must stay within plugin directory: ${relativePath}`);
|
|
83
|
+
}
|
|
84
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
85
|
+
return resolvedPath;
|
|
86
|
+
}
|
|
87
|
+
const realResolvedPath = fs.realpathSync(resolvedPath);
|
|
88
|
+
if (realResolvedPath !== normalizedPluginDir && !realResolvedPath.startsWith(`${normalizedPluginDir}${path.sep}`)) {
|
|
89
|
+
throw new Error(`'${fieldName}' resolves outside plugin directory: ${relativePath}`);
|
|
90
|
+
}
|
|
91
|
+
return realResolvedPath;
|
|
92
|
+
}
|
|
93
|
+
function parsePluginToggleConfig(pluginConfig) {
|
|
94
|
+
if (pluginConfig == null) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
if (typeof pluginConfig !== "object" || Array.isArray(pluginConfig)) {
|
|
98
|
+
throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");
|
|
99
|
+
}
|
|
100
|
+
const toggles = {};
|
|
101
|
+
for (const [key, value] of Object.entries(pluginConfig)) {
|
|
102
|
+
const normalizedKey = mustBeNonEmptyString(key, "plugins.<key>", "WEBVIEW_CONFIG");
|
|
103
|
+
if (typeof value !== "boolean") {
|
|
104
|
+
throw new Error(`'WEBVIEW_CONFIG.plugins.${normalizedKey}' must be boolean`);
|
|
105
|
+
}
|
|
106
|
+
toggles[normalizedKey] = value;
|
|
107
|
+
}
|
|
108
|
+
return toggles;
|
|
109
|
+
}
|
|
110
|
+
function selectPluginsByConfig(plugins, pluginConfig, log) {
|
|
111
|
+
const toggles = parsePluginToggleConfig(pluginConfig);
|
|
112
|
+
const matchedKeys = new Set();
|
|
113
|
+
const enabled = [];
|
|
114
|
+
for (const plugin of plugins) {
|
|
115
|
+
const selectorKeys = plugin.configKey ? [plugin.configKey, plugin.id] : [plugin.id];
|
|
116
|
+
const matches = [];
|
|
117
|
+
for (const key of selectorKeys) {
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(toggles, key)) {
|
|
119
|
+
matches.push({
|
|
120
|
+
key,
|
|
121
|
+
value: toggles[key]
|
|
122
|
+
});
|
|
123
|
+
matchedKeys.add(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const uniqueValues = [...new Set(matches.map(entry => entry.value))];
|
|
127
|
+
if (uniqueValues.length > 1) {
|
|
128
|
+
throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry => `${entry.key}=${entry.value}`).join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
const isEnabled = matches.length === 0 ? false : matches[0].value;
|
|
131
|
+
if (isEnabled) {
|
|
132
|
+
enabled.push(plugin);
|
|
133
|
+
} else {
|
|
134
|
+
log(`Plugin disabled by config: ${plugin.id}`, "info");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const unknownKeys = Object.keys(toggles).filter(key => !matchedKeys.has(key));
|
|
138
|
+
if (unknownKeys.length > 0) {
|
|
139
|
+
throw new Error(`Unknown plugin toggle key(s) in WEBVIEW_CONFIG.plugins: ${unknownKeys.join(", ")}`);
|
|
140
|
+
}
|
|
141
|
+
return enabled;
|
|
142
|
+
}
|
|
143
|
+
function validatePlugins(plugins) {
|
|
144
|
+
const pluginIds = new Set();
|
|
145
|
+
const configKeys = new Set();
|
|
146
|
+
const selectorKeys = new Map();
|
|
147
|
+
const dependencies = new Map();
|
|
148
|
+
for (const plugin of plugins) {
|
|
149
|
+
if (pluginIds.has(plugin.id)) {
|
|
150
|
+
throw new Error(`Duplicate plugin id detected: ${plugin.id}`);
|
|
151
|
+
}
|
|
152
|
+
pluginIds.add(plugin.id);
|
|
153
|
+
if (plugin.configKey) {
|
|
154
|
+
if (configKeys.has(plugin.configKey)) {
|
|
155
|
+
throw new Error(`Duplicate configKey detected: ${plugin.configKey}`);
|
|
156
|
+
}
|
|
157
|
+
configKeys.add(plugin.configKey);
|
|
158
|
+
}
|
|
159
|
+
for (const [field, selector] of [["id", plugin.id], ["configKey", plugin.configKey]]) {
|
|
160
|
+
if (!selector) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const existing = selectorKeys.get(selector);
|
|
164
|
+
if (existing && existing.pluginId !== plugin.id) {
|
|
165
|
+
throw new Error(`Plugin selector collision for '${selector}': '${existing.pluginId}' (${existing.field}) conflicts with '${plugin.id}' (${field})`);
|
|
166
|
+
}
|
|
167
|
+
selectorKeys.set(selector, {
|
|
168
|
+
pluginId: plugin.id,
|
|
169
|
+
field
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (new Set(plugin.commands).size !== plugin.commands.length) {
|
|
173
|
+
throw new Error(`Duplicate command(s) detected within plugin '${plugin.id}'`);
|
|
174
|
+
}
|
|
175
|
+
if (Object.prototype.hasOwnProperty.call(plugin.ios?.infoPlist || {}, "CFBundleURLTypes")) {
|
|
176
|
+
throw new Error(`Plugin '${plugin.id}' must use 'ios.urlSchemes' instead of 'ios.infoPlist.CFBundleURLTypes'`);
|
|
177
|
+
}
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(plugin.ios?.infoPlist || {}, "LSApplicationQueriesSchemes")) {
|
|
179
|
+
throw new Error(`Plugin '${plugin.id}' must use 'ios.querySchemes' instead of 'ios.infoPlist.LSApplicationQueriesSchemes'`);
|
|
180
|
+
}
|
|
181
|
+
for (const dependency of plugin.ios?.dependencies || []) {
|
|
182
|
+
const existing = dependencies.get(dependencyKey(dependency));
|
|
183
|
+
if (existing && requirementKey(existing.dependency) !== requirementKey(dependency)) {
|
|
184
|
+
throw new Error(`iOS dependency version conflict for '${dependency.url}': '${existing.dependency.requirement.type}:${existing.dependency.requirement.version}' in '${existing.pluginId}', '${dependency.requirement.type}:${dependency.requirement.version}' in '${plugin.id}'`);
|
|
185
|
+
}
|
|
186
|
+
if (existing && packageKey(existing.dependency) !== packageKey(dependency)) {
|
|
187
|
+
throw new Error(`iOS dependency package identity conflict for '${dependency.url}': '${existing.dependency.package}' in '${existing.pluginId}', '${dependency.package}' in '${plugin.id}'`);
|
|
188
|
+
}
|
|
189
|
+
if (!existing) {
|
|
190
|
+
dependencies.set(dependencyKey(dependency), {
|
|
191
|
+
dependency,
|
|
192
|
+
pluginId: plugin.id
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const resourcePath of plugin.ios?.resources || []) {
|
|
197
|
+
const resolvedPath = resolveManifestPath(plugin.pluginDir, resourcePath, `ios.resources for '${plugin.id}'`);
|
|
198
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
199
|
+
throw new Error(`iOS resource path not found for plugin '${plugin.id}': ${resourcePath}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function selectPluginsForPlatform(plugins, platform, log) {
|
|
205
|
+
const selected = [];
|
|
206
|
+
for (const plugin of plugins) {
|
|
207
|
+
if (plugin.platforms.includes(platform)) {
|
|
208
|
+
selected.push(plugin);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
log(`Plugin enabled but not supported on ${platform}: ${plugin.id}`, "info");
|
|
212
|
+
}
|
|
213
|
+
return selected;
|
|
214
|
+
}
|
|
215
|
+
function walkFiles(rootDir, predicate, results = []) {
|
|
216
|
+
if (!isDir(rootDir)) {
|
|
217
|
+
return results;
|
|
218
|
+
}
|
|
219
|
+
for (const entry of fs.readdirSync(rootDir, {
|
|
220
|
+
withFileTypes: true
|
|
221
|
+
})) {
|
|
222
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from readdirSync within the walked root.
|
|
223
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
walkFiles(fullPath, predicate, results);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (predicate(entry.name, fullPath)) {
|
|
229
|
+
results.push(fullPath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
234
|
+
function resolvePluginClassSourcePath(plugin) {
|
|
235
|
+
const className = plugin.ios.className.split(".").pop();
|
|
236
|
+
const candidateName = `${className}.swift`;
|
|
237
|
+
const candidates = walkFiles(plugin.ios.sourceDir, name => name === candidateName);
|
|
238
|
+
return candidates[0] || null;
|
|
239
|
+
}
|
|
240
|
+
function validateSelectedPluginSources(plugins) {
|
|
241
|
+
for (const plugin of plugins) {
|
|
242
|
+
if (!plugin.ios) {
|
|
243
|
+
throw new Error(`iOS config missing for selected plugin '${plugin.id}'`);
|
|
244
|
+
}
|
|
245
|
+
if (!isDir(plugin.ios.sourceDir)) {
|
|
246
|
+
throw new Error(`iOS source directory missing for selected plugin '${plugin.id}'`);
|
|
247
|
+
}
|
|
248
|
+
const codeFiles = walkFiles(plugin.ios.sourceDir, name => name.endsWith(".swift"));
|
|
249
|
+
if (codeFiles.length === 0) {
|
|
250
|
+
throw new Error(`No iOS source files found for selected plugin '${plugin.id}'`);
|
|
251
|
+
}
|
|
252
|
+
if (!resolvePluginClassSourcePath(plugin)) {
|
|
253
|
+
throw new Error(`Declared class '${plugin.ios.className}' for selected plugin '${plugin.id}' was not found under ${plugin.ios.sourceDir}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function collectIosDependencies(plugins) {
|
|
258
|
+
const dependenciesByUrl = new Map();
|
|
259
|
+
for (const plugin of plugins) {
|
|
260
|
+
for (const dependency of plugin.ios?.dependencies || []) {
|
|
261
|
+
const existing = dependenciesByUrl.get(dependency.url);
|
|
262
|
+
if (!existing) {
|
|
263
|
+
dependenciesByUrl.set(dependency.url, {
|
|
264
|
+
url: dependency.url,
|
|
265
|
+
package: dependency.package,
|
|
266
|
+
requirement: dependency.requirement,
|
|
267
|
+
products: [...dependency.products]
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (requirementKey(existing) !== requirementKey(dependency)) {
|
|
272
|
+
throw new Error(`iOS dependency version conflict for '${dependency.url}' while composing selected plugins`);
|
|
273
|
+
}
|
|
274
|
+
if (packageKey(existing) !== packageKey(dependency)) {
|
|
275
|
+
throw new Error(`iOS dependency package identity conflict for '${dependency.url}' while composing selected plugins`);
|
|
276
|
+
}
|
|
277
|
+
existing.products = asUniqueSorted([...existing.products, ...dependency.products]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return [...dependenciesByUrl.values()].sort((left, right) => left.url.localeCompare(right.url));
|
|
281
|
+
}
|
|
282
|
+
function normalizeResourceRelativePath(plugin, absolutePath) {
|
|
283
|
+
const relativePath = path.relative(plugin.pluginDir, absolutePath);
|
|
284
|
+
if (relativePath.startsWith(`ios${path.sep}resources${path.sep}`)) {
|
|
285
|
+
return relativePath.slice(`ios${path.sep}resources${path.sep}`.length);
|
|
286
|
+
}
|
|
287
|
+
if (relativePath.startsWith(`ios${path.sep}`)) {
|
|
288
|
+
return relativePath.slice(`ios${path.sep}`.length);
|
|
289
|
+
}
|
|
290
|
+
return relativePath;
|
|
291
|
+
}
|
|
292
|
+
function validateBundleRelativePath(pluginId, bundleRelativePath) {
|
|
293
|
+
const normalizedPath = path.posix.normalize(bundleRelativePath);
|
|
294
|
+
const expectedPrefix = `PluginResources/${sanitizeForPath(pluginId)}`;
|
|
295
|
+
if (path.posix.isAbsolute(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../")) {
|
|
296
|
+
throw new Error(`Invalid bundled resource path for plugin '${pluginId}': ${bundleRelativePath}`);
|
|
297
|
+
}
|
|
298
|
+
if (normalizedPath !== expectedPrefix && !normalizedPath.startsWith(`${expectedPrefix}/`)) {
|
|
299
|
+
throw new Error(`Bundled resource path escaped managed directory for plugin '${pluginId}': ${bundleRelativePath}`);
|
|
300
|
+
}
|
|
301
|
+
return normalizedPath;
|
|
302
|
+
}
|
|
303
|
+
function collectIosResources(plugins) {
|
|
304
|
+
const resources = [];
|
|
305
|
+
for (const plugin of plugins) {
|
|
306
|
+
for (const resourcePath of plugin.ios?.resources || []) {
|
|
307
|
+
const resolvedPath = resolveManifestPath(plugin.pluginDir, resourcePath, `ios.resources for '${plugin.id}'`);
|
|
308
|
+
const entries = fs.statSync(resolvedPath).isDirectory() ? walkFiles(resolvedPath, () => true) : [resolvedPath];
|
|
309
|
+
for (const entryPath of entries) {
|
|
310
|
+
const normalizedRelativePath = normalizeResourceRelativePath(plugin, entryPath);
|
|
311
|
+
const candidateBundleRelativePath = path
|
|
312
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The resulting path is checked by validateBundleRelativePath below.
|
|
313
|
+
.join("PluginResources", sanitizeForPath(plugin.id), normalizedRelativePath).split(path.sep).join("/");
|
|
314
|
+
const bundleRelativePath = validateBundleRelativePath(plugin.id, candidateBundleRelativePath);
|
|
315
|
+
resources.push({
|
|
316
|
+
pluginId: plugin.id,
|
|
317
|
+
sourcePath: entryPath,
|
|
318
|
+
bundleRelativePath
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return resources.sort((left, right) => left.bundleRelativePath.localeCompare(right.bundleRelativePath));
|
|
324
|
+
}
|
|
325
|
+
function collectIosInfoPlist(plugins) {
|
|
326
|
+
let infoPlist = {};
|
|
327
|
+
for (const plugin of plugins) {
|
|
328
|
+
infoPlist = mergeStructuredValues(infoPlist, plugin.ios?.infoPlist || {}, `ios.infoPlist for '${plugin.id}'`);
|
|
329
|
+
}
|
|
330
|
+
return infoPlist;
|
|
331
|
+
}
|
|
332
|
+
function collectIosEntitlements(plugins) {
|
|
333
|
+
let entitlements = {};
|
|
334
|
+
for (const plugin of plugins) {
|
|
335
|
+
entitlements = mergeStructuredValues(entitlements, plugin.ios?.entitlements || {}, `ios.entitlements for '${plugin.id}'`);
|
|
336
|
+
}
|
|
337
|
+
return entitlements;
|
|
338
|
+
}
|
|
339
|
+
function collectIosUrlSchemes(plugins) {
|
|
340
|
+
const entries = [];
|
|
341
|
+
for (const plugin of plugins) {
|
|
342
|
+
for (const entry of plugin.ios?.urlSchemes || []) {
|
|
343
|
+
entries.push({
|
|
344
|
+
name: entry.name || plugin.id,
|
|
345
|
+
schemes: asUniqueSorted(entry.schemes)
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return entries;
|
|
350
|
+
}
|
|
351
|
+
function collectIosQuerySchemes(plugins) {
|
|
352
|
+
return asUniqueSorted(plugins.flatMap(plugin => plugin.ios?.querySchemes || []));
|
|
353
|
+
}
|
|
354
|
+
function copyIosPluginSources(plugins, iosProjectPath, log) {
|
|
355
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The destination path components are fixed.
|
|
356
|
+
const internalRoot = path.join(iosProjectPath, "Sources", "Core", "Plugins", "Internal");
|
|
357
|
+
fs.rmSync(internalRoot, {
|
|
358
|
+
recursive: true,
|
|
359
|
+
force: true
|
|
360
|
+
});
|
|
361
|
+
ensureDir(internalRoot);
|
|
362
|
+
let copiedCount = 0;
|
|
363
|
+
for (const plugin of plugins) {
|
|
364
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - plugin.id is sanitized before being used as a path component.
|
|
365
|
+
const pluginOutputDir = path.join(internalRoot, sanitizeForPath(plugin.id));
|
|
366
|
+
const codeFiles = walkFiles(plugin.ios.sourceDir, name => name.endsWith(".swift"));
|
|
367
|
+
for (const sourcePath of codeFiles) {
|
|
368
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - sourcePath is produced by walking plugin.ios.sourceDir.
|
|
369
|
+
const targetPath = path.join(pluginOutputDir, path.relative(plugin.ios.sourceDir, sourcePath));
|
|
370
|
+
ensureDir(path.dirname(targetPath));
|
|
371
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
372
|
+
copiedCount++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
log(`Copied ${copiedCount} iOS plugin source file(s)`, "info");
|
|
376
|
+
}
|
|
377
|
+
function formatSwiftDictionary(entries, emptyLiteral = "[:]") {
|
|
378
|
+
return entries.length === 0 ? emptyLiteral : `[\n${entries.join(",\n")}\n ]`;
|
|
379
|
+
}
|
|
380
|
+
function generatePluginRegistryFiles(plugins, iosProjectPath) {
|
|
381
|
+
const pluginFactories = {};
|
|
382
|
+
const pluginToCommands = {};
|
|
383
|
+
for (const plugin of plugins) {
|
|
384
|
+
pluginFactories[plugin.id] = plugin.ios.className;
|
|
385
|
+
pluginToCommands[plugin.id] = asUniqueSorted(plugin.commands);
|
|
386
|
+
}
|
|
387
|
+
const factoryEntries = Object.keys(pluginFactories).sort().map(pluginId => ` ${JSON.stringify(pluginId)}: { ${pluginFactories[pluginId]}() }`);
|
|
388
|
+
const commandEntries = Object.keys(pluginToCommands).sort().map(pluginId => {
|
|
389
|
+
const commands = pluginToCommands[pluginId];
|
|
390
|
+
const commandSet = commands.length ? `Set([${commands.map(value => JSON.stringify(value)).join(", ")}])` : "[]";
|
|
391
|
+
return ` ${JSON.stringify(pluginId)}: ${commandSet}`;
|
|
392
|
+
});
|
|
393
|
+
const indexContent = `import Foundation
|
|
9
394
|
|
|
10
395
|
enum GeneratedPluginIndex {
|
|
11
396
|
static let pluginFactories: [String: () -> CatalystPlugin] = ${formatSwiftDictionary(factoryEntries)}
|
|
12
397
|
static let pluginToCommands: [String: Set<String>] = ${formatSwiftDictionary(commandEntries)}
|
|
13
398
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
399
|
+
`;
|
|
400
|
+
|
|
401
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The destination path components are fixed.
|
|
402
|
+
const pluginsDir = path.join(iosProjectPath, "Sources", "Core", "Plugins");
|
|
403
|
+
ensureDir(pluginsDir);
|
|
404
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename is fixed.
|
|
405
|
+
fs.writeFileSync(path.join(pluginsDir, "GeneratedPluginIndex.swift"), indexContent);
|
|
406
|
+
}
|
|
407
|
+
function composeIosPlugins({
|
|
408
|
+
corePluginsRoot,
|
|
409
|
+
iosProjectPath,
|
|
410
|
+
pluginConfig,
|
|
411
|
+
log
|
|
412
|
+
}) {
|
|
413
|
+
const discovered = discoverInternalPlugins(corePluginsRoot, log);
|
|
414
|
+
validatePlugins(discovered);
|
|
415
|
+
const enabled = selectPluginsByConfig(discovered, pluginConfig, log);
|
|
416
|
+
const selected = selectPluginsForPlatform(enabled, "ios", log);
|
|
417
|
+
validateSelectedPluginSources(selected);
|
|
418
|
+
const iosDependencies = collectIosDependencies(selected);
|
|
419
|
+
const infoPlist = collectIosInfoPlist(selected);
|
|
420
|
+
const urlSchemes = collectIosUrlSchemes(selected);
|
|
421
|
+
const querySchemes = collectIosQuerySchemes(selected);
|
|
422
|
+
const entitlements = collectIosEntitlements(selected);
|
|
423
|
+
const resources = collectIosResources(selected);
|
|
424
|
+
copyIosPluginSources(selected, iosProjectPath, log);
|
|
425
|
+
generatePluginRegistryFiles(selected, iosProjectPath);
|
|
426
|
+
log(`Plugin composition complete (${selected.length} enabled iOS plugin(s))`, "success");
|
|
427
|
+
return {
|
|
428
|
+
pluginCount: selected.length,
|
|
429
|
+
commandCount: selected.reduce((total, plugin) => total + plugin.commands.length, 0),
|
|
430
|
+
iosDependencies,
|
|
431
|
+
infoPlist,
|
|
432
|
+
urlSchemes,
|
|
433
|
+
querySchemes,
|
|
434
|
+
entitlements,
|
|
435
|
+
resources
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
module.exports = {
|
|
439
|
+
composeIosPlugins
|
|
440
|
+
};
|