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,16 +1,332 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
function createAssetsPhase(ctx) {
|
|
6
|
+
const {
|
|
7
|
+
configPath,
|
|
8
|
+
publicPath,
|
|
9
|
+
pwd,
|
|
10
|
+
progress
|
|
11
|
+
} = ctx;
|
|
12
|
+
async function copyBuildAssets(androidConfig, buildOptimisation = false) {
|
|
13
|
+
if (!buildOptimisation) return;
|
|
14
|
+
progress.log("Copying build assets to Android project...", "info");
|
|
15
|
+
try {
|
|
16
|
+
const sourcePath = `${process.cwd()}/build/`;
|
|
17
|
+
const destPath = `${pwd}/androidProject/app/src/main/assets/build/`;
|
|
18
|
+
ctx.runCommand(`mkdir -p ${destPath}`);
|
|
19
|
+
ctx.runCommand(`rm -rf ${destPath}/*`);
|
|
20
|
+
const excludePatterns = ["route-manifest.json.gz", "route-manifest.json.br"];
|
|
21
|
+
if (buildOptimisation) {
|
|
22
|
+
progress.log("Running with build optimization...", "info");
|
|
23
|
+
const excludeParams = excludePatterns.map(pattern => `--exclude="${pattern}"`).join(" ");
|
|
24
|
+
const rsyncCommand = `rsync -av ${excludeParams} ${sourcePath} ${destPath}`;
|
|
25
|
+
progress.log("Executing rsync command with exclusions...", "info");
|
|
26
|
+
ctx.runCommand(rsyncCommand);
|
|
27
|
+
for (const pattern of excludePatterns) {
|
|
28
|
+
const checkCommand = `find ${destPath} -name "${pattern}" | wc -l`;
|
|
29
|
+
const count = parseInt(ctx.runCommand(checkCommand).trim(), 10);
|
|
30
|
+
if (count > 0) {
|
|
31
|
+
progress.log(`Warning: Found ${count} instances of excluded file ${pattern}`, "warning");
|
|
32
|
+
ctx.runCommand(`find ${destPath} -name "${pattern}" -delete`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
progress.log("Build assets copied with optimization (excluded route-manifest JSON files)", "success");
|
|
36
|
+
} else {
|
|
37
|
+
progress.log("Running without build optimization...", "info");
|
|
38
|
+
const exclusions = excludePatterns.map(pattern => `-not -name "${pattern}"`).join(" ");
|
|
39
|
+
const copyCommand = `find ${sourcePath} -type f ${exclusions} -exec cp --parents {} ${destPath} \\;`;
|
|
40
|
+
progress.log(`Executing copy command with exclusions...`, "info");
|
|
41
|
+
ctx.runCommand(copyCommand);
|
|
42
|
+
progress.log("Build assets copied successfully!", "success");
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error("Error copying build assets: " + error.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function copySplashscreenAssets() {
|
|
49
|
+
try {
|
|
50
|
+
const destPath = `${pwd}/androidProject/app/src/main/res`;
|
|
51
|
+
const configFile = fs.readFileSync(configPath, "utf8");
|
|
52
|
+
const config = JSON.parse(configFile);
|
|
53
|
+
const {
|
|
54
|
+
WEBVIEW_CONFIG = {}
|
|
55
|
+
} = config;
|
|
56
|
+
if (!WEBVIEW_CONFIG.splashScreen) return;
|
|
57
|
+
const androidPublicPath = `${process.cwd()}/public/android`;
|
|
58
|
+
const imageFormats = ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
|
|
59
|
+
if (fs.existsSync(androidPublicPath)) {
|
|
60
|
+
const drawableDir = `${destPath}/drawable`;
|
|
61
|
+
if (!fs.existsSync(drawableDir)) {
|
|
62
|
+
fs.mkdirSync(drawableDir, {
|
|
63
|
+
recursive: true
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const format of imageFormats) {
|
|
67
|
+
const existingPath = `${destPath}/drawable/splashscreen.${format}`;
|
|
68
|
+
if (fs.existsSync(existingPath)) {
|
|
69
|
+
fs.unlinkSync(existingPath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const format of imageFormats) {
|
|
73
|
+
const sourcePath = `${androidPublicPath}/splashscreen.${format}`;
|
|
74
|
+
if (fs.existsSync(sourcePath)) {
|
|
75
|
+
fs.copyFileSync(sourcePath, `${destPath}/drawable/splashscreen.${format}`);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const backgroundColor = WEBVIEW_CONFIG.splashScreen.backgroundColor || "#ffffff";
|
|
81
|
+
const themeFiles = [`${destPath}/values/themes.xml`, `${destPath}/values-night/themes.xml`];
|
|
82
|
+
for (const themesFile of themeFiles) {
|
|
83
|
+
try {
|
|
84
|
+
let content = fs.readFileSync(themesFile, "utf8");
|
|
85
|
+
content = content.replace(/<item name="android:windowBackground">.*?<\/item>/, `<item name="android:windowBackground">${backgroundColor}</item>`);
|
|
86
|
+
content = content.replace(/<item name="android:windowSplashScreenBackground" tools:targetApi="31">.*?<\/item>/, `<item name="android:windowSplashScreenBackground" tools:targetApi="31">${backgroundColor}</item>`);
|
|
87
|
+
fs.writeFileSync(themesFile, content);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (e.code !== "ENOENT") throw e;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
progress.log(`Error processing splash screen: ${error.message}`, "warning");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function copyOfflinePage() {
|
|
97
|
+
try {
|
|
98
|
+
const sourcePath = `${process.cwd()}/public/offline.html`;
|
|
99
|
+
const destDir = `${pwd}/androidProject/app/src/main/assets/offline`;
|
|
100
|
+
const destPath = `${destDir}/offline.html`;
|
|
101
|
+
if (!fs.existsSync(sourcePath)) {
|
|
102
|
+
progress.log("offline.html not found in public/; skipping offline asset copy", "warning");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
ctx.runCommand(`mkdir -p ${destDir}`);
|
|
106
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
107
|
+
progress.log("offline.html copied to Android assets", "success");
|
|
108
|
+
} catch (error) {
|
|
109
|
+
progress.log(`Warning: Error copying offline.html: ${error.message}`, "warning");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function copyIconAssets() {
|
|
113
|
+
try {
|
|
114
|
+
const destPath = `${pwd}/androidProject/app/src/main/res`;
|
|
115
|
+
const manifestPath = `${pwd}/androidProject/app/src/main/AndroidManifest.xml`;
|
|
116
|
+
const androidIconDir = path.join(publicPath, "android", "appIcons"); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - publicPath is process.cwd()/public, a trusted internal path.
|
|
117
|
+
const fallbackIconPath = path.join(__dirname, "../assets", "catalyst.png"); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - __dirname is a fixed module path, not user input.
|
|
118
|
+
const fallbackExists = fs.existsSync(fallbackIconPath);
|
|
119
|
+
const extensions = ["png", "jpg", "jpeg"];
|
|
120
|
+
if (!fs.existsSync(publicPath)) {
|
|
121
|
+
progress.log(`Warning: Public directory not found at ${publicPath}`, "warning");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const densities = [{
|
|
125
|
+
key: "mdpi",
|
|
126
|
+
dir: "mipmap-mdpi"
|
|
127
|
+
}, {
|
|
128
|
+
key: "hdpi",
|
|
129
|
+
dir: "mipmap-hdpi"
|
|
130
|
+
}, {
|
|
131
|
+
key: "xhdpi",
|
|
132
|
+
dir: "mipmap-xhdpi"
|
|
133
|
+
}, {
|
|
134
|
+
key: "xxhdpi",
|
|
135
|
+
dir: "mipmap-xxhdpi"
|
|
136
|
+
}, {
|
|
137
|
+
key: "xxxhdpi",
|
|
138
|
+
dir: "mipmap-xxxhdpi"
|
|
139
|
+
}];
|
|
140
|
+
const hasIconDirectory = fs.existsSync(androidIconDir) && fs.lstatSync(androidIconDir).isDirectory();
|
|
141
|
+
const cleanDensityDir = dir => {
|
|
142
|
+
if (!fs.existsSync(dir)) return;
|
|
143
|
+
for (const file of fs.readdirSync(dir)) {
|
|
144
|
+
if (file.startsWith("icon.")) {
|
|
145
|
+
fs.unlinkSync(path.join(dir, file)); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is derived from pwd/destPath (internal), file comes from fs.readdirSync on that same dir.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const findIconForDensity = densityKey => {
|
|
150
|
+
if (!hasIconDirectory) return null;
|
|
151
|
+
for (const ext of extensions) {
|
|
152
|
+
const candidate = path.join(androidIconDir, `icon-${densityKey}.${ext}`); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - androidIconDir is derived from publicPath (internal); densityKey and ext are fixed internal strings.
|
|
153
|
+
if (fs.existsSync(candidate)) return {
|
|
154
|
+
path: candidate,
|
|
155
|
+
ext
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
};
|
|
160
|
+
let hasCustomIcons = false;
|
|
161
|
+
let usedFallback = false;
|
|
162
|
+
for (const density of densities) {
|
|
163
|
+
const targetDir = path.join(destPath, density.dir); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); density.dir is a fixed string from the densities array.
|
|
164
|
+
const source = findIconForDensity(density.key);
|
|
165
|
+
cleanDensityDir(targetDir);
|
|
166
|
+
if (source) {
|
|
167
|
+
fs.mkdirSync(targetDir, {
|
|
168
|
+
recursive: true
|
|
169
|
+
});
|
|
170
|
+
const destination = path.join(targetDir, `icon.${source.ext}`); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); source.ext is from a fixed extensions array.
|
|
171
|
+
fs.copyFileSync(source.path, destination);
|
|
172
|
+
hasCustomIcons = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!hasCustomIcons && fallbackExists) {
|
|
176
|
+
const targetDir = path.join(destPath, "mipmap-xxxhdpi"); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); "mipmap-xxxhdpi" is a fixed literal.
|
|
177
|
+
fs.mkdirSync(targetDir, {
|
|
178
|
+
recursive: true
|
|
179
|
+
});
|
|
180
|
+
cleanDensityDir(targetDir);
|
|
181
|
+
const destination = path.join(targetDir, "icon.png"); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); "icon.png" is a fixed literal.
|
|
182
|
+
fs.copyFileSync(fallbackIconPath, destination);
|
|
183
|
+
usedFallback = true;
|
|
184
|
+
}
|
|
185
|
+
const setManifestIcons = (iconValue, roundIconValue) => {
|
|
186
|
+
let current;
|
|
187
|
+
try {
|
|
188
|
+
current = fs.readFileSync(manifestPath, "utf8");
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (e.code === "ENOENT") return;
|
|
191
|
+
throw e;
|
|
192
|
+
}
|
|
193
|
+
const updated = current.replace(/android:icon="[^"]*"/g, `android:icon="${iconValue}"`).replace(/android:roundIcon="[^"]*"/g, `android:roundIcon="${roundIconValue}"`);
|
|
194
|
+
if (updated !== current) fs.writeFileSync(manifestPath, updated);
|
|
195
|
+
};
|
|
196
|
+
if (!hasCustomIcons && !usedFallback) {
|
|
197
|
+
progress.log("No custom Android icons found; using default template icons.", "info");
|
|
198
|
+
setManifestIcons("@mipmap/ic_launcher", "@mipmap/ic_launcher_round");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setManifestIcons("@mipmap/icon", "@mipmap/icon");
|
|
202
|
+
if (hasCustomIcons) {
|
|
203
|
+
progress.log("Applied Android launcher icons from public/android/appIcons.", "success");
|
|
204
|
+
}
|
|
205
|
+
if (usedFallback) {
|
|
206
|
+
progress.log("Used bundled Catalyst fallback icon for launcher.", "info");
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
progress.log(`Warning: Error copying app icon assets: ${error.message}`, "warning");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function configureAppName(androidConfig) {
|
|
213
|
+
try {
|
|
214
|
+
const destPath = `${pwd}/androidProject/app/src/main/res`;
|
|
215
|
+
const stringsFile = `${destPath}/values/strings.xml`;
|
|
216
|
+
let stringsContent = fs.readFileSync(stringsFile, "utf8");
|
|
217
|
+
if (androidConfig.appName) {
|
|
218
|
+
stringsContent = stringsContent.replace(/<string name="app_name">.*?<\/string>/, `<string name="app_name">${androidConfig.appName}</string>`);
|
|
219
|
+
fs.writeFileSync(stringsFile, stringsContent);
|
|
220
|
+
progress.log(`App display name configured: ${androidConfig.appName}`, "success");
|
|
221
|
+
} else {
|
|
222
|
+
stringsContent = stringsContent.replace(/<string name="app_name">.*?<\/string>/, `<string name="app_name">Catalyst Application</string>`);
|
|
223
|
+
fs.writeFileSync(stringsFile, stringsContent);
|
|
224
|
+
progress.log("App display name reverted to default: Catalyst Application", "info");
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
progress.log(`Warning: Error configuring app name: ${error.message}`, "warning");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Notification helpers ──────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
async function handleGoogleServicesJson() {
|
|
234
|
+
try {
|
|
235
|
+
const rootGoogleServicesPath = `${process.cwd()}/google-services.json`;
|
|
236
|
+
const androidGoogleServicesPath = `${pwd}/androidProject/app/google-services.json`;
|
|
237
|
+
if (fs.existsSync(rootGoogleServicesPath)) {
|
|
238
|
+
progress.log("Found google-services.json in root directory", "info");
|
|
239
|
+
const appDir = `${pwd}/androidProject/app`;
|
|
240
|
+
if (!fs.existsSync(appDir)) {
|
|
241
|
+
fs.mkdirSync(appDir, {
|
|
242
|
+
recursive: true
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
fs.copyFileSync(rootGoogleServicesPath, androidGoogleServicesPath);
|
|
246
|
+
progress.log("Copied google-services.json to androidProject/app/", "success");
|
|
247
|
+
return true;
|
|
248
|
+
} else if (fs.existsSync(androidGoogleServicesPath)) {
|
|
249
|
+
progress.log("google-services.json already exists in androidProject/app/", "info");
|
|
250
|
+
return true;
|
|
251
|
+
} else {
|
|
252
|
+
progress.log("google-services.json not found - Firebase push notifications will not work", "warning");
|
|
253
|
+
progress.log("Place google-services.json in project root or src/native/androidProject/app/", "info");
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
progress.log(`Warning: Error handling google-services.json: ${error.message}`, "warning");
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function processNotifications(WEBVIEW_CONFIG) {
|
|
262
|
+
const hasNotificationConfig = !!WEBVIEW_CONFIG.notifications?.enabled;
|
|
263
|
+
try {
|
|
264
|
+
await writeGeneratedBuildProperties(hasNotificationConfig);
|
|
265
|
+
await cleanupNotificationPermissions();
|
|
266
|
+
await cleanupNotificationResources();
|
|
267
|
+
await cleanupNotificationMetadata();
|
|
268
|
+
await cleanupNotificationAssets();
|
|
269
|
+
if (!hasNotificationConfig) {
|
|
270
|
+
progress.log("Notifications disabled - cleaned up notification configurations", "info");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const hasGoogleServices = await handleGoogleServicesJson();
|
|
274
|
+
if (!hasGoogleServices) {
|
|
275
|
+
progress.log("Continuing without Firebase - only local notifications will work", "warning");
|
|
276
|
+
}
|
|
277
|
+
await addNotificationPermissions();
|
|
278
|
+
await generateNotificationResources(WEBVIEW_CONFIG.notifications);
|
|
279
|
+
await addNotificationMetadata();
|
|
280
|
+
await processNotificationAssets();
|
|
281
|
+
progress.log("Notification configuration completed successfully!", "success");
|
|
282
|
+
} catch (error) {
|
|
283
|
+
progress.log(`Warning: Error processing notifications: ${error.message}`, "warning");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function writeGeneratedBuildProperties(notificationsEnabled) {
|
|
287
|
+
try {
|
|
288
|
+
const propertiesPath = `${pwd}/androidProject/app/catalyst-build.properties`;
|
|
289
|
+
const propertiesContent = ["# Generated by Catalyst. Used by Android Studio builds when -PconfigPath is not provided.", `notifications.enabled=${notificationsEnabled ? "true" : "false"}`, ""].join("\n");
|
|
290
|
+
fs.writeFileSync(propertiesPath, propertiesContent);
|
|
291
|
+
progress.log("Updated generated Android build properties", "success");
|
|
292
|
+
} catch (error) {
|
|
293
|
+
throw new Error(`Failed to write generated Android build properties: ${error.message}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function addNotificationPermissions() {
|
|
297
|
+
try {
|
|
298
|
+
const manifestPath = `${pwd}/androidProject/app/src/main/AndroidManifest.xml`;
|
|
299
|
+
let manifestContent = fs.readFileSync(manifestPath, "utf8");
|
|
300
|
+
const permissionsXml = ` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
11
301
|
<uses-permission android:name="android.permission.VIBRATE" />
|
|
12
302
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
13
|
-
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;
|
|
303
|
+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;
|
|
304
|
+
manifestContent = manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/, `$1\n${permissionsXml}$2`);
|
|
305
|
+
fs.writeFileSync(manifestPath, manifestContent);
|
|
306
|
+
progress.log("Added notification permissions to AndroidManifest.xml", "success");
|
|
307
|
+
} catch (error) {
|
|
308
|
+
throw new Error(`Failed to add notification permissions: ${error.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function generateNotificationResources(notificationConfig) {
|
|
312
|
+
try {
|
|
313
|
+
const colorsPath = `${pwd}/androidProject/app/src/main/res/values/colors.xml`;
|
|
314
|
+
let colorsContent = fs.readFileSync(colorsPath, "utf8");
|
|
315
|
+
const notificationColorXml = ` <color name="notification_color">${notificationConfig.color || "#007AFF"}</color>`;
|
|
316
|
+
if (!colorsContent.includes('name="notification_color"')) {
|
|
317
|
+
colorsContent = colorsContent.replace(/(<\/resources>)/, `${notificationColorXml}\n$1`);
|
|
318
|
+
fs.writeFileSync(colorsPath, colorsContent);
|
|
319
|
+
progress.log("Added notification color to colors.xml", "success");
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
throw new Error(`Failed to generate notification resources: ${error.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function addNotificationMetadata() {
|
|
326
|
+
try {
|
|
327
|
+
const manifestPath = `${pwd}/androidProject/app/src/main/AndroidManifest.xml`;
|
|
328
|
+
let manifestContent = fs.readFileSync(manifestPath, "utf8");
|
|
329
|
+
const metadataXml = `
|
|
14
330
|
<!-- Default notification configuration -->
|
|
15
331
|
<meta-data
|
|
16
332
|
android:name="default_notification_channel_id"
|
|
@@ -40,7 +356,101 @@ async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${pr
|
|
|
40
356
|
<intent-filter>
|
|
41
357
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
42
358
|
</intent-filter>
|
|
43
|
-
</service>`;
|
|
359
|
+
</service>`;
|
|
360
|
+
manifestContent = manifestContent.replace(/(\s*<\/application>)/, `${metadataXml}\n$1`);
|
|
361
|
+
fs.writeFileSync(manifestPath, manifestContent);
|
|
362
|
+
progress.log("Added notification metadata and push notification service to AndroidManifest.xml", "success");
|
|
363
|
+
} catch (error) {
|
|
364
|
+
throw new Error(`Failed to add notification metadata: ${error.message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function processNotificationAssets() {
|
|
368
|
+
try {
|
|
369
|
+
const destPath = `${pwd}/androidProject/app/src/main/res`;
|
|
370
|
+
const imageFormats = ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"];
|
|
371
|
+
const audioFormats = ["mp3", "wav", "ogg"];
|
|
372
|
+
let assetsProcessed = 0;
|
|
373
|
+
const notificationIcons = [{
|
|
374
|
+
sourceName: "notification-icon",
|
|
375
|
+
resourceName: "ic_notification"
|
|
376
|
+
}, {
|
|
377
|
+
sourceName: "notification-large",
|
|
378
|
+
resourceName: "ic_notification_large"
|
|
379
|
+
}];
|
|
380
|
+
const notificationSounds = [{
|
|
381
|
+
sourceName: "notification-sound-default",
|
|
382
|
+
resourceName: "notification_sound_default"
|
|
383
|
+
}, {
|
|
384
|
+
sourceName: "notification-sound-urgent",
|
|
385
|
+
resourceName: "notification_sound_urgent"
|
|
386
|
+
}];
|
|
387
|
+
for (const icon of notificationIcons) {
|
|
388
|
+
for (const format of imageFormats) {
|
|
389
|
+
const existingIconPath = `${destPath}/drawable/${icon.resourceName}.${format}`;
|
|
390
|
+
if (fs.existsSync(existingIconPath)) {
|
|
391
|
+
fs.unlinkSync(existingIconPath);
|
|
392
|
+
progress.log(`Removed existing ${icon.resourceName}.${format}`, "info");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const sound of notificationSounds) {
|
|
397
|
+
for (const format of audioFormats) {
|
|
398
|
+
const existingSoundPath = `${destPath}/raw/${sound.resourceName}.${format}`;
|
|
399
|
+
if (fs.existsSync(existingSoundPath)) {
|
|
400
|
+
fs.unlinkSync(existingSoundPath);
|
|
401
|
+
progress.log(`Removed existing ${sound.resourceName}.${format}`, "info");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
let iconFound = false;
|
|
406
|
+
for (const icon of notificationIcons) {
|
|
407
|
+
for (const format of imageFormats) {
|
|
408
|
+
const iconImagePath = `${publicPath}/${icon.sourceName}.${format}`;
|
|
409
|
+
if (fs.existsSync(iconImagePath)) {
|
|
410
|
+
const destImagePath = `${destPath}/drawable/${icon.resourceName}.${format}`;
|
|
411
|
+
fs.copyFileSync(iconImagePath, destImagePath);
|
|
412
|
+
progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}.${format}`, "success");
|
|
413
|
+
assetsProcessed++;
|
|
414
|
+
if (icon.sourceName === "notification-icon") {
|
|
415
|
+
iconFound = true;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!iconFound) {
|
|
422
|
+
generateNotificationIconDrawable("ic_notification", destPath);
|
|
423
|
+
progress.log("Generated default notification icon", "info");
|
|
424
|
+
}
|
|
425
|
+
const rawDir = `${destPath}/raw`;
|
|
426
|
+
if (!fs.existsSync(rawDir)) {
|
|
427
|
+
fs.mkdirSync(rawDir, {
|
|
428
|
+
recursive: true
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
for (const sound of notificationSounds) {
|
|
432
|
+
for (const format of audioFormats) {
|
|
433
|
+
const soundImagePath = `${publicPath}/${sound.sourceName}.${format}`;
|
|
434
|
+
if (fs.existsSync(soundImagePath)) {
|
|
435
|
+
const destSoundPath = `${destPath}/raw/${sound.resourceName}.${format}`;
|
|
436
|
+
fs.copyFileSync(soundImagePath, destSoundPath);
|
|
437
|
+
progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`, "success");
|
|
438
|
+
assetsProcessed++;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (assetsProcessed > 0) {
|
|
444
|
+
progress.log(`Processed ${assetsProcessed} notification assets from public/`, "success");
|
|
445
|
+
} else {
|
|
446
|
+
progress.log("No notification assets found in public/ - using defaults", "info");
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
throw new Error(`Failed to process notification assets: ${error.message}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function generateNotificationIconDrawable(iconName, destPath) {
|
|
453
|
+
const iconXml = `<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
44
454
|
android:width="24dp"
|
|
45
455
|
android:height="24dp"
|
|
46
456
|
android:viewportWidth="24"
|
|
@@ -49,4 +459,91 @@ async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${pr
|
|
|
49
459
|
<path
|
|
50
460
|
android:fillColor="@android:color/white"
|
|
51
461
|
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
|
52
|
-
</vector>`;
|
|
462
|
+
</vector>`;
|
|
463
|
+
fs.writeFileSync(`${destPath}/drawable/${iconName}.xml`, iconXml);
|
|
464
|
+
}
|
|
465
|
+
async function cleanupNotificationPermissions() {
|
|
466
|
+
try {
|
|
467
|
+
const manifestPath = `${pwd}/androidProject/app/src/main/AndroidManifest.xml`;
|
|
468
|
+
let manifestContent = fs.readFileSync(manifestPath, "utf8");
|
|
469
|
+
const notificationPermissions = ["android.permission.POST_NOTIFICATIONS", "android.permission.VIBRATE", "android.permission.WAKE_LOCK", "android.permission.RECEIVE_BOOT_COMPLETED"];
|
|
470
|
+
manifestContent = manifestContent.split("\n").filter(line => {
|
|
471
|
+
return !notificationPermissions.some(permission => line.includes(`<uses-permission android:name="${permission}"`));
|
|
472
|
+
}).join("\n");
|
|
473
|
+
fs.writeFileSync(manifestPath, manifestContent);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
progress.log(`Warning: Error cleaning notification permissions: ${error.message}`, "warning");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function cleanupNotificationResources() {
|
|
479
|
+
try {
|
|
480
|
+
const colorsPath = `${pwd}/androidProject/app/src/main/res/values/colors.xml`;
|
|
481
|
+
let colorsContent = fs.readFileSync(colorsPath, "utf8");
|
|
482
|
+
const existingColorLine = colorsContent.split("\n").find(line => line.includes('name="notification_color"'));
|
|
483
|
+
if (existingColorLine) {
|
|
484
|
+
colorsContent = colorsContent.replace(existingColorLine, "");
|
|
485
|
+
fs.writeFileSync(colorsPath, colorsContent);
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
progress.log(`Warning: Error cleaning notification resources: ${error.message}`, "warning");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function cleanupNotificationMetadata() {
|
|
492
|
+
try {
|
|
493
|
+
const manifestPath = `${pwd}/androidProject/app/src/main/AndroidManifest.xml`;
|
|
494
|
+
let manifestContent = fs.readFileSync(manifestPath, "utf8");
|
|
495
|
+
const metadataNames = ["default_notification_channel_id", "default_notification_icon", "default_notification_color", "com.google.firebase.messaging.default_notification_channel_id", "com.google.firebase.messaging.default_notification_icon", "com.google.firebase.messaging.default_notification_sound"];
|
|
496
|
+
for (const metadataName of metadataNames) {
|
|
497
|
+
const metadataRegex = new RegExp(`\\s*<meta-data\\s+[\\s\\S]*?android:name="${metadataName}"[\\s\\S]*?\\/>`, "gi");
|
|
498
|
+
manifestContent = manifestContent.replace(metadataRegex, "");
|
|
499
|
+
}
|
|
500
|
+
const serviceRegex = /\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;
|
|
501
|
+
manifestContent = manifestContent.replace(serviceRegex, "");
|
|
502
|
+
manifestContent = manifestContent.replace(/\s*<!--\s*Default notification configuration\s*-->/gi, "");
|
|
503
|
+
manifestContent = manifestContent.replace(/\s*<!--\s*Firebase default notification configuration\s*-->/gi, "");
|
|
504
|
+
manifestContent = manifestContent.replace(/\s*<!--\s*Push Notification Service\s*-->/gi, "");
|
|
505
|
+
fs.writeFileSync(manifestPath, manifestContent);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
progress.log(`Warning: Error cleaning notification metadata: ${error.message}`, "warning");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function cleanupNotificationAssets() {
|
|
511
|
+
try {
|
|
512
|
+
const destPath = `${pwd}/androidProject/app/src/main/res`;
|
|
513
|
+
const imageFormats = ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "xml"];
|
|
514
|
+
const audioFormats = ["mp3", "wav", "ogg"];
|
|
515
|
+
const notificationIcons = ["ic_notification", "ic_notification_large"];
|
|
516
|
+
const notificationSounds = ["notification_sound_default", "notification_sound_urgent"];
|
|
517
|
+
for (const icon of notificationIcons) {
|
|
518
|
+
for (const format of imageFormats) {
|
|
519
|
+
const iconPath = `${destPath}/drawable/${icon}.${format}`;
|
|
520
|
+
if (fs.existsSync(iconPath)) {
|
|
521
|
+
fs.unlinkSync(iconPath);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const rawDir = `${destPath}/raw`;
|
|
526
|
+
if (fs.existsSync(rawDir)) {
|
|
527
|
+
for (const sound of notificationSounds) {
|
|
528
|
+
for (const format of audioFormats) {
|
|
529
|
+
const soundPath = `${rawDir}/${sound}.${format}`;
|
|
530
|
+
if (fs.existsSync(soundPath)) {
|
|
531
|
+
fs.unlinkSync(soundPath);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} catch (error) {
|
|
537
|
+
progress.log(`Warning: Error cleaning notification assets: ${error.message}`, "warning");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
copyBuildAssets,
|
|
542
|
+
copySplashscreenAssets,
|
|
543
|
+
copyOfflinePage,
|
|
544
|
+
copyIconAssets,
|
|
545
|
+
configureAppName,
|
|
546
|
+
processNotifications
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
module.exports = createAssetsPhase;
|