catalyst-core 0.2.0-beta.2 → 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/androidProject/app/build.gradle.kts +1 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
- package/dist/native/androidSetup.js +300 -8
- package/dist/native/bridge/WebBridge.js +481 -59
- package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
- 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 +85 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
- package/dist/native/bridge/perf/core/constants.js +126 -0
- package/dist/native/bridge/perf/core/measure.js +189 -0
- package/dist/native/bridge/perf/core/session.js +100 -0
- package/dist/native/bridge/perf/core/store.js +554 -0
- package/dist/native/bridge/perf/core/summary.js +174 -0
- package/dist/native/bridge/perf/core/traceExport.js +240 -0
- package/dist/native/bridge/perf/core/waterfalls.js +146 -0
- package/dist/native/bridge/perf/index.js +402 -0
- 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/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
- 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,28 +1,412 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
discoverInternalPlugins
|
|
7
|
+
} = require("./internalPluginUtils.js");
|
|
8
|
+
const PLUGINS_PACKAGE_PARTS = ["io", "yourname", "androidproject", "plugins"];
|
|
9
|
+
function isDir(dirPath) {
|
|
10
|
+
return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
|
|
11
|
+
}
|
|
12
|
+
function ensureDir(dirPath) {
|
|
13
|
+
if (!fs.existsSync(dirPath)) {
|
|
14
|
+
fs.mkdirSync(dirPath, {
|
|
15
|
+
recursive: true
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function sanitizeForPath(value) {
|
|
20
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
21
|
+
}
|
|
22
|
+
function mustBeNonEmptyString(value, fieldName, sourcePath) {
|
|
23
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
24
|
+
throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);
|
|
25
|
+
}
|
|
26
|
+
return value.trim();
|
|
27
|
+
}
|
|
28
|
+
function asUniqueSorted(values) {
|
|
29
|
+
return [...new Set(values)].sort();
|
|
30
|
+
}
|
|
31
|
+
function parsePluginToggleConfig(pluginConfig) {
|
|
32
|
+
if (pluginConfig == null) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
if (typeof pluginConfig !== "object" || Array.isArray(pluginConfig)) {
|
|
36
|
+
throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");
|
|
37
|
+
}
|
|
38
|
+
const toggles = {};
|
|
39
|
+
for (const [key, value] of Object.entries(pluginConfig)) {
|
|
40
|
+
const normalizedKey = mustBeNonEmptyString(key, "plugins.<key>", "WEBVIEW_CONFIG");
|
|
41
|
+
if (typeof value !== "boolean") {
|
|
42
|
+
throw new Error(`'WEBVIEW_CONFIG.plugins.${normalizedKey}' must be boolean`);
|
|
43
|
+
}
|
|
44
|
+
toggles[normalizedKey] = value;
|
|
45
|
+
}
|
|
46
|
+
return toggles;
|
|
47
|
+
}
|
|
48
|
+
function selectPluginsByConfig(plugins, pluginConfig, log) {
|
|
49
|
+
const toggles = parsePluginToggleConfig(pluginConfig);
|
|
50
|
+
const matchedKeys = new Set();
|
|
51
|
+
const selected = [];
|
|
52
|
+
for (const plugin of plugins) {
|
|
53
|
+
const selectorKeys = plugin.configKey ? [plugin.configKey, plugin.id] : [plugin.id];
|
|
54
|
+
const matches = [];
|
|
55
|
+
for (const key of selectorKeys) {
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(toggles, key)) {
|
|
57
|
+
matches.push({
|
|
58
|
+
key,
|
|
59
|
+
value: toggles[key]
|
|
60
|
+
});
|
|
61
|
+
matchedKeys.add(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const uniqueValues = [...new Set(matches.map(entry => entry.value))];
|
|
65
|
+
if (uniqueValues.length > 1) {
|
|
66
|
+
throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry => `${entry.key}=${entry.value}`).join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
const enabled = matches.length === 0 ? false : matches[0].value;
|
|
69
|
+
if (enabled) {
|
|
70
|
+
selected.push(plugin);
|
|
71
|
+
} else {
|
|
72
|
+
log(`Plugin disabled by config: ${plugin.id}`, "info");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const unknownKeys = Object.keys(toggles).filter(key => !matchedKeys.has(key));
|
|
76
|
+
if (unknownKeys.length > 0) {
|
|
77
|
+
throw new Error(`Unknown plugin toggle key(s) in WEBVIEW_CONFIG.plugins: ${unknownKeys.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
return selected;
|
|
80
|
+
}
|
|
81
|
+
function parseDependency(dependency, pluginId) {
|
|
82
|
+
const parts = dependency.split(":");
|
|
83
|
+
if (parts.length < 3) {
|
|
84
|
+
throw new Error(`Dependency '${dependency}' in plugin '${pluginId}' must be in 'group:artifact:version' format`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
key: `${parts[0]}:${parts[1]}`,
|
|
88
|
+
version: parts.slice(2).join(":")
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function validatePlugins(plugins) {
|
|
92
|
+
const pluginIds = new Set();
|
|
93
|
+
const configKeys = new Set();
|
|
94
|
+
const dependencies = new Map();
|
|
95
|
+
const selectorKeys = new Map();
|
|
96
|
+
for (const plugin of plugins) {
|
|
97
|
+
if (pluginIds.has(plugin.id)) {
|
|
98
|
+
throw new Error(`Duplicate plugin id detected: ${plugin.id}`);
|
|
99
|
+
}
|
|
100
|
+
pluginIds.add(plugin.id);
|
|
101
|
+
if (plugin.configKey) {
|
|
102
|
+
if (configKeys.has(plugin.configKey)) {
|
|
103
|
+
throw new Error(`Duplicate configKey detected: ${plugin.configKey}`);
|
|
104
|
+
}
|
|
105
|
+
configKeys.add(plugin.configKey);
|
|
106
|
+
}
|
|
107
|
+
for (const [field, selector] of [["id", plugin.id], ["configKey", plugin.configKey]]) {
|
|
108
|
+
if (!selector) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const existing = selectorKeys.get(selector);
|
|
112
|
+
if (existing && existing.pluginId !== plugin.id) {
|
|
113
|
+
throw new Error(`Plugin selector collision for '${selector}': '${existing.pluginId}' (${existing.field}) conflicts with '${plugin.id}' (${field})`);
|
|
114
|
+
}
|
|
115
|
+
selectorKeys.set(selector, {
|
|
116
|
+
pluginId: plugin.id,
|
|
117
|
+
field
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (new Set(plugin.commands).size !== plugin.commands.length) {
|
|
121
|
+
throw new Error(`Duplicate command(s) detected within plugin '${plugin.id}'`);
|
|
122
|
+
}
|
|
123
|
+
for (const dependency of plugin.android?.dependencies || []) {
|
|
124
|
+
const parsed = parseDependency(dependency, plugin.id);
|
|
125
|
+
const existing = dependencies.get(parsed.key);
|
|
126
|
+
if (existing && existing.version !== parsed.version) {
|
|
127
|
+
throw new Error(`Dependency version conflict for '${parsed.key}': '${existing.version}' in '${existing.pluginId}', '${parsed.version}' in '${plugin.id}'`);
|
|
128
|
+
}
|
|
129
|
+
if (!existing) {
|
|
130
|
+
dependencies.set(parsed.key, {
|
|
131
|
+
version: parsed.version,
|
|
132
|
+
pluginId: plugin.id
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function selectPluginsForPlatform(plugins, platform, log) {
|
|
139
|
+
const selected = [];
|
|
140
|
+
for (const plugin of plugins) {
|
|
141
|
+
if (plugin.platforms.includes(platform)) {
|
|
142
|
+
selected.push(plugin);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
log(`Plugin enabled but not supported on ${platform}: ${plugin.id}`, "info");
|
|
146
|
+
}
|
|
147
|
+
return selected;
|
|
148
|
+
}
|
|
149
|
+
function walkFiles(rootDir, predicate, results = []) {
|
|
150
|
+
if (!isDir(rootDir)) {
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
for (const entry of fs.readdirSync(rootDir, {
|
|
154
|
+
withFileTypes: true
|
|
155
|
+
})) {
|
|
156
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from readdirSync within the walked root.
|
|
157
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
158
|
+
if (entry.isDirectory()) {
|
|
159
|
+
walkFiles(fullPath, predicate, results);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (predicate(entry.name, fullPath)) {
|
|
163
|
+
results.push(fullPath);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
function resolvePluginClassSourcePath(plugin) {
|
|
169
|
+
const className = plugin.android.className.split(".").pop();
|
|
170
|
+
const candidateNames = new Set([`${className}.kt`, `${className}.java`]);
|
|
171
|
+
const candidates = walkFiles(plugin.android.sourceDir, name => candidateNames.has(name));
|
|
172
|
+
if (candidates.length > 1) {
|
|
173
|
+
throw new Error(`Declared class '${plugin.android.className}' for selected plugin '${plugin.id}' resolved to multiple source files under ${plugin.android.sourceDir}`);
|
|
174
|
+
}
|
|
175
|
+
return candidates[0] || null;
|
|
176
|
+
}
|
|
177
|
+
function validateSelectedPluginSources(plugins) {
|
|
178
|
+
for (const plugin of plugins) {
|
|
179
|
+
if (!plugin.android) {
|
|
180
|
+
throw new Error(`Android config missing for selected plugin '${plugin.id}'`);
|
|
181
|
+
}
|
|
182
|
+
if (!isDir(plugin.android.sourceDir)) {
|
|
183
|
+
throw new Error(`Android source directory missing for selected plugin '${plugin.id}'`);
|
|
184
|
+
}
|
|
185
|
+
const codeFiles = walkFiles(plugin.android.sourceDir, name => name.endsWith(".kt") || name.endsWith(".java"));
|
|
186
|
+
if (codeFiles.length === 0) {
|
|
187
|
+
throw new Error(`No Android source files found for selected plugin '${plugin.id}'`);
|
|
188
|
+
}
|
|
189
|
+
if (!resolvePluginClassSourcePath(plugin)) {
|
|
190
|
+
throw new Error(`Declared class '${plugin.android.className}' for selected plugin '${plugin.id}' was not found as a .kt or .java file under ${plugin.android.sourceDir}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function copyTree(sourceDir, targetDir) {
|
|
195
|
+
if (!isDir(sourceDir)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
ensureDir(targetDir);
|
|
199
|
+
for (const filePath of walkFiles(sourceDir, () => true)) {
|
|
200
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - filePath is produced by walking sourceDir, so the relative path remains within that tree.
|
|
201
|
+
const targetPath = path.join(targetDir, path.relative(sourceDir, filePath));
|
|
202
|
+
ensureDir(path.dirname(targetPath));
|
|
203
|
+
fs.copyFileSync(filePath, targetPath);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function copyAndroidPluginSources(plugins, javaRoot, log) {
|
|
207
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The package path components are fixed constants.
|
|
208
|
+
const internalRoot = path.join(javaRoot, ...PLUGINS_PACKAGE_PARTS, "internal");
|
|
209
|
+
fs.rmSync(internalRoot, {
|
|
210
|
+
recursive: true,
|
|
211
|
+
force: true
|
|
212
|
+
});
|
|
213
|
+
ensureDir(internalRoot);
|
|
214
|
+
let copiedCount = 0;
|
|
215
|
+
for (const plugin of plugins) {
|
|
216
|
+
// 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.
|
|
217
|
+
const pluginOutputDir = path.join(internalRoot, sanitizeForPath(plugin.id));
|
|
218
|
+
const codeFiles = walkFiles(plugin.android.sourceDir, name => name.endsWith(".kt") || name.endsWith(".java"));
|
|
219
|
+
for (const sourcePath of codeFiles) {
|
|
220
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - sourcePath is produced by walking plugin.android.sourceDir.
|
|
221
|
+
const targetPath = path.join(pluginOutputDir, path.relative(plugin.android.sourceDir, sourcePath));
|
|
222
|
+
ensureDir(path.dirname(targetPath));
|
|
223
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
224
|
+
copiedCount++;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
log(`Copied ${copiedCount} Android plugin source file(s)`, "info");
|
|
228
|
+
}
|
|
229
|
+
function copyPluginAssets(plugins, androidProjectPath, log) {
|
|
230
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The destination path components are fixed.
|
|
231
|
+
const baseAssetsDir = path.join(androidProjectPath, "app", "src", "main", "assets", "plugins");
|
|
232
|
+
fs.rmSync(baseAssetsDir, {
|
|
233
|
+
recursive: true,
|
|
234
|
+
force: true
|
|
235
|
+
});
|
|
236
|
+
ensureDir(baseAssetsDir);
|
|
237
|
+
for (const plugin of plugins) {
|
|
238
|
+
// 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.
|
|
239
|
+
const pluginAssetsDir = path.join(baseAssetsDir, sanitizeForPath(plugin.id));
|
|
240
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed asset directories beneath the discovered internal plugin directory.
|
|
241
|
+
copyTree(path.join(plugin.pluginDir, "assets", "common"), path.join(pluginAssetsDir, "common"));
|
|
242
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed asset directories beneath the discovered internal plugin directory.
|
|
243
|
+
copyTree(path.join(plugin.pluginDir, "assets", "android"), path.join(pluginAssetsDir, "android"));
|
|
244
|
+
}
|
|
245
|
+
log("Plugin assets copied to app/src/main/assets/plugins", "info");
|
|
246
|
+
}
|
|
247
|
+
function formatKotlinMap(entries, emptyLiteral = "emptyMap()") {
|
|
248
|
+
return entries.length === 0 ? emptyLiteral : `mapOf(\n${entries.join(",\n")}\n )`;
|
|
249
|
+
}
|
|
250
|
+
function generatePluginRegistryFiles(plugins, javaRoot) {
|
|
251
|
+
const pluginIdToClassName = {};
|
|
252
|
+
const pluginToCommands = {};
|
|
253
|
+
for (const plugin of plugins) {
|
|
254
|
+
pluginIdToClassName[plugin.id] = plugin.android.className;
|
|
255
|
+
pluginToCommands[plugin.id] = asUniqueSorted(plugin.commands);
|
|
256
|
+
}
|
|
257
|
+
const classEntries = Object.keys(pluginIdToClassName).sort().map(pluginId => ` ${JSON.stringify(pluginId)} to ${JSON.stringify(pluginIdToClassName[pluginId])}`);
|
|
258
|
+
const commandEntries = Object.keys(pluginToCommands).sort().map(pluginId => {
|
|
259
|
+
const commands = pluginToCommands[pluginId];
|
|
260
|
+
const commandSet = commands.length ? `setOf(${commands.map(value => JSON.stringify(value)).join(", ")})` : "emptySet()";
|
|
261
|
+
return ` ${JSON.stringify(pluginId)} to ${commandSet}`;
|
|
262
|
+
});
|
|
263
|
+
const indexContent = `package io.yourname.androidproject.plugins
|
|
11
264
|
|
|
12
265
|
object GeneratedPluginIndex {
|
|
13
266
|
val pluginIdToClassName: Map<String, String> = ${formatKotlinMap(classEntries)}
|
|
14
267
|
val pluginToCommands: Map<String, Set<String>> = ${formatKotlinMap(commandEntries)}
|
|
15
268
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The package path components are fixed constants.
|
|
272
|
+
const pluginsDir = path.join(javaRoot, ...PLUGINS_PACKAGE_PARTS);
|
|
273
|
+
ensureDir(pluginsDir);
|
|
274
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename is fixed.
|
|
275
|
+
fs.writeFileSync(path.join(pluginsDir, "GeneratedPluginIndex.kt"), indexContent);
|
|
276
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename is fixed.
|
|
277
|
+
fs.rmSync(path.join(pluginsDir, "GeneratedPluginMeta.kt"), {
|
|
278
|
+
force: true
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function escapeRegexLiteral(value) {
|
|
282
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
283
|
+
}
|
|
284
|
+
function updateAndroidManifestPermissions(manifestPath, selectedPermissions, allKnownPluginPermissions) {
|
|
285
|
+
const uniquePermissions = asUniqueSorted(selectedPermissions);
|
|
286
|
+
const knownPermissions = asUniqueSorted(allKnownPluginPermissions);
|
|
287
|
+
let manifest = fs.readFileSync(manifestPath, "utf8");
|
|
288
|
+
const beginMarker = "<!-- CATALYST_PLUGIN_PERMISSIONS_START -->";
|
|
289
|
+
const endMarker = "<!-- CATALYST_PLUGIN_PERMISSIONS_END -->";
|
|
290
|
+
const markerRegex = /[ \t]*<!-- CATALYST_PLUGIN_PERMISSIONS_START -->[\s\S]*?<!-- CATALYST_PLUGIN_PERMISSIONS_END -->\s*/g;
|
|
291
|
+
manifest = manifest.replace(markerRegex, "");
|
|
292
|
+
|
|
293
|
+
// Migration cleanup for legacy entries previously written without markers.
|
|
294
|
+
for (const permission of knownPermissions) {
|
|
295
|
+
const escaped = escapeRegexLiteral(permission);
|
|
296
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - The dynamic value is escaped as a regex literal above.
|
|
297
|
+
const legacyRegex = new RegExp(`^[ \\t]*<uses-permission\\s+android:name="${escaped}"\\s*/>\\s*\\n?`, "gm");
|
|
298
|
+
manifest = manifest.replace(legacyRegex, "");
|
|
299
|
+
}
|
|
300
|
+
if (uniquePermissions.length > 0) {
|
|
301
|
+
const permissionLines = uniquePermissions.map(permission => ` <uses-permission android:name="${permission}" />`).join("\n");
|
|
302
|
+
const managedBlock = ` ${beginMarker}\n${permissionLines}\n ${endMarker}\n`;
|
|
303
|
+
manifest = manifest.replace(/<application\b/, `${managedBlock} <application`);
|
|
304
|
+
}
|
|
305
|
+
fs.writeFileSync(manifestPath, manifest);
|
|
306
|
+
}
|
|
307
|
+
function findDependenciesBlockRange(gradleText, gradlePath) {
|
|
308
|
+
const headerMatch = gradleText.match(/^dependencies\s*\{/m);
|
|
309
|
+
if (!headerMatch || headerMatch.index == null) {
|
|
310
|
+
throw new Error(`Could not find dependencies block in ${gradlePath}`);
|
|
311
|
+
}
|
|
312
|
+
const openBraceIndex = gradleText.indexOf("{", headerMatch.index);
|
|
313
|
+
if (openBraceIndex === -1) {
|
|
314
|
+
throw new Error(`Malformed dependencies block in ${gradlePath}`);
|
|
315
|
+
}
|
|
316
|
+
let depth = 0;
|
|
317
|
+
for (let index = openBraceIndex; index < gradleText.length; index++) {
|
|
318
|
+
const ch = gradleText[index];
|
|
319
|
+
if (ch === "{") depth++;
|
|
320
|
+
if (ch === "}") depth--;
|
|
321
|
+
if (depth === 0) {
|
|
322
|
+
return {
|
|
323
|
+
openBraceIndex,
|
|
324
|
+
blockEnd: index
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw new Error(`Could not find end of dependencies block in ${gradlePath}`);
|
|
329
|
+
}
|
|
330
|
+
function updateGradleDependencies(gradlePath, selectedDependencies, allKnownPluginDependencies) {
|
|
331
|
+
const uniqueDependencies = asUniqueSorted(selectedDependencies);
|
|
332
|
+
const knownDependencies = asUniqueSorted(allKnownPluginDependencies);
|
|
333
|
+
let gradle = fs.readFileSync(gradlePath, "utf8");
|
|
334
|
+
const {
|
|
335
|
+
openBraceIndex,
|
|
336
|
+
blockEnd
|
|
337
|
+
} = findDependenciesBlockRange(gradle, gradlePath);
|
|
338
|
+
let blockBody = gradle.slice(openBraceIndex + 1, blockEnd);
|
|
339
|
+
const beginMarker = "// CATALYST_PLUGIN_DEPENDENCIES_START";
|
|
340
|
+
const endMarker = "// CATALYST_PLUGIN_DEPENDENCIES_END";
|
|
341
|
+
const markerRegex = /[ \t]*\/\/ CATALYST_PLUGIN_DEPENDENCIES_START[\s\S]*?\/\/ CATALYST_PLUGIN_DEPENDENCIES_END\s*/g;
|
|
342
|
+
blockBody = blockBody.replace(markerRegex, "");
|
|
343
|
+
|
|
344
|
+
// Migration cleanup for legacy entries previously written without markers.
|
|
345
|
+
for (const dependency of knownDependencies) {
|
|
346
|
+
const escaped = escapeRegexLiteral(dependency);
|
|
347
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - The dynamic value is escaped as a regex literal above.
|
|
348
|
+
const legacyRegex = new RegExp(`^[ \\t]*implementation\\("${escaped}"\\)\\s*\\n?`, "gm");
|
|
349
|
+
blockBody = blockBody.replace(legacyRegex, "");
|
|
350
|
+
}
|
|
351
|
+
if (uniqueDependencies.length > 0) {
|
|
352
|
+
const managedLines = uniqueDependencies.map(dependency => ` implementation("${dependency}")`).join("\n");
|
|
353
|
+
blockBody = `${blockBody}\n ${beginMarker}\n${managedLines}\n ${endMarker}\n`;
|
|
354
|
+
}
|
|
355
|
+
gradle = `${gradle.slice(0, openBraceIndex + 1)}${blockBody}${gradle.slice(blockEnd)}`;
|
|
356
|
+
fs.writeFileSync(gradlePath, gradle);
|
|
357
|
+
}
|
|
358
|
+
function updateProguardKeepRules(proguardPath, selectedClassNames, allKnownPluginClassNames) {
|
|
359
|
+
const uniqueClassNames = asUniqueSorted(selectedClassNames);
|
|
360
|
+
const knownClassNames = asUniqueSorted(allKnownPluginClassNames);
|
|
361
|
+
let rules = fs.readFileSync(proguardPath, "utf8");
|
|
362
|
+
const beginMarker = "# CATALYST_PLUGIN_KEEP_START";
|
|
363
|
+
const endMarker = "# CATALYST_PLUGIN_KEEP_END";
|
|
364
|
+
const markerRegex = /[ \t]*# CATALYST_PLUGIN_KEEP_START[\s\S]*?# CATALYST_PLUGIN_KEEP_END\s*/g;
|
|
365
|
+
rules = rules.replace(markerRegex, "");
|
|
366
|
+
for (const className of knownClassNames) {
|
|
367
|
+
const escaped = escapeRegexLiteral(className);
|
|
368
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - The dynamic value is escaped as a regex literal above.
|
|
369
|
+
const legacyRegex = new RegExp(`^[ \\t]*-keep class ${escaped} \\{ \\*; \\}\\s*\\n?`, "gm");
|
|
370
|
+
rules = rules.replace(legacyRegex, "");
|
|
371
|
+
}
|
|
372
|
+
if (uniqueClassNames.length > 0) {
|
|
373
|
+
const keepLines = uniqueClassNames.map(className => `-keep class ${className} { *; }`).join("\n");
|
|
374
|
+
rules = `${rules.trimEnd()}\n\n${beginMarker}\n${keepLines}\n${endMarker}\n`;
|
|
375
|
+
}
|
|
376
|
+
fs.writeFileSync(proguardPath, rules);
|
|
377
|
+
}
|
|
378
|
+
function composeAndroidPlugins({
|
|
379
|
+
corePluginsRoot,
|
|
380
|
+
androidProjectPath,
|
|
381
|
+
pluginConfig,
|
|
382
|
+
log
|
|
383
|
+
}) {
|
|
384
|
+
const discovered = discoverInternalPlugins(corePluginsRoot, log);
|
|
385
|
+
validatePlugins(discovered);
|
|
386
|
+
const enabled = selectPluginsByConfig(discovered, pluginConfig, log);
|
|
387
|
+
const selected = selectPluginsForPlatform(enabled, "android", log);
|
|
388
|
+
validateSelectedPluginSources(selected);
|
|
389
|
+
|
|
390
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed paths within the generated Android project.
|
|
391
|
+
const javaRoot = path.join(androidProjectPath, "app", "src", "main", "java");
|
|
392
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed paths within the generated Android project.
|
|
393
|
+
const manifestPath = path.join(androidProjectPath, "app", "src", "main", "AndroidManifest.xml");
|
|
394
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed paths within the generated Android project.
|
|
395
|
+
const gradlePath = path.join(androidProjectPath, "app", "build.gradle.kts");
|
|
396
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed paths within the generated Android project.
|
|
397
|
+
const proguardPath = path.join(androidProjectPath, "app", "proguard-rules.pro");
|
|
398
|
+
copyAndroidPluginSources(selected, javaRoot, log);
|
|
399
|
+
copyPluginAssets(selected, androidProjectPath, log);
|
|
400
|
+
generatePluginRegistryFiles(selected, javaRoot);
|
|
401
|
+
updateAndroidManifestPermissions(manifestPath, selected.flatMap(plugin => plugin.android?.permissions || []), discovered.flatMap(plugin => plugin.android?.permissions || []));
|
|
402
|
+
updateGradleDependencies(gradlePath, selected.flatMap(plugin => plugin.android?.dependencies || []), discovered.flatMap(plugin => plugin.android?.dependencies || []));
|
|
403
|
+
updateProguardKeepRules(proguardPath, selected.map(plugin => plugin.android?.className).filter(Boolean), discovered.map(plugin => plugin.android?.className).filter(Boolean));
|
|
404
|
+
log(`Plugin composition complete (${selected.length} enabled plugin(s))`, "success");
|
|
405
|
+
return {
|
|
406
|
+
pluginCount: selected.length,
|
|
407
|
+
commandCount: selected.reduce((total, plugin) => total + plugin.commands.length, 0)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
module.exports = {
|
|
411
|
+
composeAndroidPlugins
|
|
412
|
+
};
|