catalyst-core 0.1.0-beta.4 → 0.1.0-beta.5
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";const{execFile,execSync,execFileSync}=require("child_process");const fs=require("fs");const path=require("path");const TerminalProgress=require("./TerminalProgress.js").default;const crypto=require("crypto");const{runInteractiveCommand}=require("./utils.js");const{composeIosPlugins}=require("./pluginComposerIos.js");const{resolveInternalPluginsRoot,resolvePluginConfig}=require("./internalPluginUtils.js");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const pwd=path.join(catalystCorePath,"dist/native");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);// Configuration constants
|
|
2
|
-
const iosConfig=WEBVIEW_CONFIG.ios;const isGoogleSignInEnabled=WEBVIEW_CONFIG.googleSignIn?.enabled??false;const protocol=WEBVIEW_CONFIG.useHttps?"https":"http";const ip=WEBVIEW_CONFIG.LOCAL_IP||"localhost";const port=WEBVIEW_CONFIG.port?WEBVIEW_CONFIG.useHttps?443:WEBVIEW_CONFIG.port:null;let url=port?`${protocol}://${ip}:${port}`:`${protocol}://${ip}`;const PUBLIC_PATH=`${process.env.PWD}/public`;const PROJECT_DIR=`${pwd}/iosnativeWebView`;const SCHEME_NAME="iosnativeWebView";const APP_BUNDLE_ID=iosConfig.appBundleId||"com.debug.webview";const PROJECT_NAME=path.basename(PROJECT_DIR);const IPHONE_MODEL=iosConfig.simulatorName;const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";const MANAGED_BASELINE_SUFFIX=".catalyst-base";const PLUGIN_RESOURCE_ROOT="PluginResources";function packageRequirementKey(dependency){return`${dependency.requirement.type}:${dependency.requirement.version}`;}function mergePackageDependency(map,dependency,sourceLabel){const existing=map.get(dependency.url);if(!existing){map.set(dependency.url,{url:dependency.url,package:dependency.package,requirement:dependency.requirement,products:[...new Set(dependency.products)]});return;}if(packageRequirementKey(existing)!==packageRequirementKey(dependency)){throw new Error(`iOS package dependency conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.requirement.type}:${existing.requirement.version}' vs '${dependency.requirement.type}:${dependency.requirement.version}'`);}if(existing.package!==dependency.package){throw new Error(`iOS package identity conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.package}' vs '${dependency.package}'`);}existing.products=[...new Set([...existing.products,...dependency.products])].sort();}function formatSwiftPackageRequirement(dependency){if(dependency.requirement.type==="from"){return`from: "${dependency.requirement.version}"`;}if(dependency.requirement.type==="exact"){return`exact: "${dependency.requirement.version}"`;}throw new Error(`Unsupported iOS package requirement type: ${dependency.requirement.type}`);}function formatSwiftPackageEntries(dependencies){return dependencies.map(dependency=>` .package(url: "${dependency.url}", ${formatSwiftPackageRequirement(dependency)})`);}function formatSwiftProductEntries(dependencies){return dependencies.flatMap(dependency=>dependency.products.map(product=>` .product(name: "${product}", package: "${dependency.package}")`));}function isPlainObject(value){return!!value&&typeof value==="object"&&!Array.isArray(value);}function deepEqual(left,right){return JSON.stringify(left)===JSON.stringify(right);}function mergeStructuredValues(existing,incoming,fieldName){if(existing===undefined){return JSON.parse(JSON.stringify(incoming));}if(Array.isArray(existing)&&Array.isArray(incoming)){const merged=[];const seen=new Set();for(const value of[...existing,...incoming]){const key=JSON.stringify(value);if(seen.has(key)){continue;}seen.add(key);merged.push(value);}return merged;}if(isPlainObject(existing)&&isPlainObject(incoming)){const merged={...existing};for(const[key,value]of Object.entries(incoming)){merged[key]=mergeStructuredValues(merged[key],value,`${fieldName}.${key}`);}return merged;}if(deepEqual(existing,incoming)){return existing;}throw new Error(`Conflicting values for '${fieldName}' while composing iOS build metadata`);}function ensureManagedBaseline(filePath){const baselinePath=`${filePath}${MANAGED_BASELINE_SUFFIX}`;if(!fs.existsSync(baselinePath)){if(!fs.existsSync(filePath)){throw new Error(`Managed baseline source file not found: ${filePath}`);}fs.copyFileSync(filePath,baselinePath);}return baselinePath;}function restoreManagedFileFromBaseline(filePath){const baselinePath=ensureManagedBaseline(filePath);fs.copyFileSync(baselinePath,filePath);}function readPlistObject(filePath){const output=execFileSync("plutil",["-convert","json","-o","-",filePath],{encoding:"utf8"});return JSON.parse(output);}function writePlistObject(filePath,value){
|
|
2
|
+
const iosConfig=WEBVIEW_CONFIG.ios;const isGoogleSignInEnabled=WEBVIEW_CONFIG.googleSignIn?.enabled??false;const protocol=WEBVIEW_CONFIG.useHttps?"https":"http";const ip=WEBVIEW_CONFIG.LOCAL_IP||"localhost";const port=WEBVIEW_CONFIG.port?WEBVIEW_CONFIG.useHttps?443:WEBVIEW_CONFIG.port:null;let url=port?`${protocol}://${ip}:${port}`:`${protocol}://${ip}`;const PUBLIC_PATH=`${process.env.PWD}/public`;const PROJECT_DIR=`${pwd}/iosnativeWebView`;const SCHEME_NAME="iosnativeWebView";const APP_BUNDLE_ID=iosConfig.appBundleId||"com.debug.webview";const PROJECT_NAME=path.basename(PROJECT_DIR);const IPHONE_MODEL=iosConfig.simulatorName;const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";const MANAGED_BASELINE_SUFFIX=".catalyst-base";const PLUGIN_RESOURCE_ROOT="PluginResources";function packageRequirementKey(dependency){return`${dependency.requirement.type}:${dependency.requirement.version}`;}function mergePackageDependency(map,dependency,sourceLabel){const existing=map.get(dependency.url);if(!existing){map.set(dependency.url,{url:dependency.url,package:dependency.package,requirement:dependency.requirement,products:[...new Set(dependency.products)]});return;}if(packageRequirementKey(existing)!==packageRequirementKey(dependency)){throw new Error(`iOS package dependency conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.requirement.type}:${existing.requirement.version}' vs '${dependency.requirement.type}:${dependency.requirement.version}'`);}if(existing.package!==dependency.package){throw new Error(`iOS package identity conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.package}' vs '${dependency.package}'`);}existing.products=[...new Set([...existing.products,...dependency.products])].sort();}function formatSwiftPackageRequirement(dependency){if(dependency.requirement.type==="from"){return`from: "${dependency.requirement.version}"`;}if(dependency.requirement.type==="exact"){return`exact: "${dependency.requirement.version}"`;}throw new Error(`Unsupported iOS package requirement type: ${dependency.requirement.type}`);}function formatSwiftPackageEntries(dependencies){return dependencies.map(dependency=>` .package(url: "${dependency.url}", ${formatSwiftPackageRequirement(dependency)})`);}function formatSwiftProductEntries(dependencies){return dependencies.flatMap(dependency=>dependency.products.map(product=>` .product(name: "${product}", package: "${dependency.package}")`));}function isPlainObject(value){return!!value&&typeof value==="object"&&!Array.isArray(value);}function deepEqual(left,right){return JSON.stringify(left)===JSON.stringify(right);}function mergeStructuredValues(existing,incoming,fieldName){if(existing===undefined){return JSON.parse(JSON.stringify(incoming));}if(Array.isArray(existing)&&Array.isArray(incoming)){const merged=[];const seen=new Set();for(const value of[...existing,...incoming]){const key=JSON.stringify(value);if(seen.has(key)){continue;}seen.add(key);merged.push(value);}return merged;}if(isPlainObject(existing)&&isPlainObject(incoming)){const merged={...existing};for(const[key,value]of Object.entries(incoming)){merged[key]=mergeStructuredValues(merged[key],value,`${fieldName}.${key}`);}return merged;}if(deepEqual(existing,incoming)){return existing;}throw new Error(`Conflicting values for '${fieldName}' while composing iOS build metadata`);}function ensureManagedBaseline(filePath){const baselinePath=`${filePath}${MANAGED_BASELINE_SUFFIX}`;if(!fs.existsSync(baselinePath)){if(!fs.existsSync(filePath)){throw new Error(`Managed baseline source file not found: ${filePath}`);}fs.copyFileSync(filePath,baselinePath);}return baselinePath;}function restoreManagedFileFromBaseline(filePath){const baselinePath=ensureManagedBaseline(filePath);fs.copyFileSync(baselinePath,filePath);}function readPlistObject(filePath){const output=execFileSync("plutil",["-convert","json","-o","-",filePath],{encoding:"utf8"});return JSON.parse(output);}function writePlistObject(filePath,value){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename uses path.basename and stays under the fixed project directory.
|
|
3
|
+
const tempPath=path.join(PROJECT_DIR,`.catalyst-${path.basename(filePath)}-${process.pid}.json`);fs.writeFileSync(tempPath,JSON.stringify(value,null,2),"utf8");try{execFileSync("plutil",["-convert","xml1","-o",filePath,tempPath]);}finally{fs.rmSync(tempPath,{force:true});}}function mergeIntoTopLevelObject(target,source,fieldName){for(const[key,value]of Object.entries(source||{})){target[key]=mergeStructuredValues(target[key],value,`${fieldName}.${key}`);}}function mergeUrlSchemes(plistObject,entries){const existing=Array.isArray(plistObject.CFBundleURLTypes)?plistObject.CFBundleURLTypes:[];const merged=existing.map(entry=>({...entry,CFBundleURLSchemes:Array.isArray(entry?.CFBundleURLSchemes)?[...new Set(entry.CFBundleURLSchemes)]:[]}));for(const entry of entries){const targetName=entry.name||null;let match=merged.find(item=>item.CFBundleURLName===targetName);if(!match){match={CFBundleURLName:targetName,CFBundleURLSchemes:[]};merged.push(match);}match.CFBundleURLSchemes=[...new Set([...match.CFBundleURLSchemes,...entry.schemes])];}if(merged.length>0){plistObject.CFBundleURLTypes=merged;}}function mergeQuerySchemes(plistObject,schemes){const existing=Array.isArray(plistObject.LSApplicationQueriesSchemes)?plistObject.LSApplicationQueriesSchemes:[];const merged=[...new Set([...existing,...schemes.filter(Boolean)])];if(merged.length>0){plistObject.LSApplicationQueriesSchemes=merged;}}function formatPbxprojPath(value){return /[^A-Za-z0-9_./-]/.test(value)?`"${value}"`:value;}function detectPbxprojFileType(filePath){const extension=path.extname(filePath).toLowerCase();const fileTypes={".json":"text.json",".plist":"text.plist.xml",".txt":"text",".html":"text.html",".js":"sourcecode.javascript",".css":"text.css",".png":"image.png",".jpg":"image.jpeg",".jpeg":"image.jpeg",".gif":"image.gif",".svg":"image.svg",".mp3":"audio.mp3",".wav":"audio.wav",".m4a":"audio.m4a",".caf":"audio.caf",".pdf":"image.pdf"};return fileTypes[extension]||"file";}function generateProjectObjectId(identifier,suffix=""){return crypto.createHash("md5").update(`${identifier}:${suffix}`).digest("hex").substring(0,24).toUpperCase();}function getXcodeProjectFilePath(){return path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");}function replaceRequired(content,pattern,replacement,errorMessage){if(!pattern.test(content)){throw new Error(errorMessage);}return content.replace(pattern,replacement);}// Define build steps for progress tracking
|
|
3
4
|
const steps={config:"Generating Required Configuration for build",deviceDetection:"Detecting Physical Device",launchSimulator:"Launch iOS Simulator",clean:"Clean Build Artifacts",assets:"Process Notification Assets",build:"Build IOS Project",findApp:"Locate Built Application",install:"Install Application",launch:"Launch Application"};// Configure progress display
|
|
4
5
|
const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new TerminalProgress(steps,"Catalyst iOS Build",progressConfig);const shellCommand=process.platform==="win32"?"cmd.exe":"sh";const shellArgs=command=>process.platform==="win32"?["/d","/s","/c",command]:["-c",command];// Utility function to run shell commands
|
|
5
6
|
function runCommand(command,options={}){return new Promise((resolve,reject)=>{execFile(shellCommand,shellArgs(command),{maxBuffer:1024*1024*10,...options},(error,stdout,stderr)=>{if(error){console.error(`Command failed: ${command}`);console.error(`Error: ${error.message}`);console.error(`stderr: ${stderr}`);reject(error);return;}if(stderr){console.warn(`Warning: ${stderr}`);}resolve(stdout.trim());});});}async function getBootedSimulatorUUID(modelName){const UUID_RE=/[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/i;try{const output=await runInteractiveCommand("xcrun",["simctl","list","devices"],{});const lines=output.split("\n");// First pass: find a booted simulator matching modelName
|
|
@@ -59,7 +60,8 @@ projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCo
|
|
|
59
60
|
projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCore in Frameworks \*\/,)/,`$1\n\t\t\t\t${NOTIF_BUILD_FILE_ID} /* CatalystNotifications in Frameworks */,`);// 3. Add to packageProductDependencies array
|
|
60
61
|
projectContent=projectContent.replace(/(packageProductDependencies = \(\s*C99974332E97D56900C25611 \/\* CatalystCore \*\/,)/,`$1\n\t\t\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */,`);// 4. Add to XCSwiftPackageProductDependency section
|
|
61
62
|
projectContent=projectContent.replace(/(\/\* End XCSwiftPackageProductDependency section \*\/)/,`\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = C99974322E97D56900C25611 /* XCLocalSwiftPackageReference "." */;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n$1`);fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications added to Xcode project","success");}else if(!isNotificationsEnabled&&hasNotifications){progress.log("Removing CatalystNotifications from Xcode project","info");// Remove all CatalystNotifications entries
|
|
62
|
-
projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/ = {isa = PBXBuildFile; productRef = [A-F0-9]+ \/\* CatalystNotifications \*\/; };\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = [A-F0-9]+ \/\* XCLocalSwiftPackageReference "." \*\/;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n/g,"");fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications removed from Xcode project","success");}else{progress.log("Package dependencies already correct","info");}}catch(error){progress.log(`❌ Failed to update package dependencies: ${error.message}`,"error");throw error;}}async function updateInfoPlist(pluginComposition={}){try{const infoPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info.plist");const infoReleasePlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info-Release.plist");const googleServicesPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,GOOGLE_SERVICES_FILENAME);const googleClientId=WEBVIEW_CONFIG.googleSignIn?.clientId||WEBVIEW_CONFIG.googleSignIn?.webClientId||"";const iosClientId=WEBVIEW_CONFIG.googleSignIn?.iosClientId||"";const googleServicesContent=fs.existsSync(googleServicesPlistPath)?fs.readFileSync(googleServicesPlistPath,"utf8"):null;const reversedClientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>REVERSED_CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const clientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const computeReversed=value=>{if(!value)return"";return value.split(".").reverse().join(".");};const resolvedClientIdForScheme=iosClientId||googleClientId||clientIdFromServices||"";const resolvedReversedClientId=reversedClientIdFromServices||computeReversed(resolvedClientIdForScheme);if(isGoogleSignInEnabled&&!resolvedReversedClientId){progress.fail("config","Google Sign-In enabled but no valid clientId found");process.exit(1);}const plistTargets=[infoPlistPath,infoReleasePlistPath];const pluginUrlSchemes=pluginComposition.urlSchemes||[];const pluginQuerySchemes=pluginComposition.querySchemes||[];const pluginInfoPlist=pluginComposition.infoPlist||{};plistTargets.forEach(plistPath=>{if(!fs.existsSync(plistPath)){return;}restoreManagedFileFromBaseline(plistPath);const plistObject=readPlistObject(plistPath);plistObject.CFBundleDisplayName=iosConfig.appName||"Catalyst Application";mergeIntoTopLevelObject(plistObject,pluginInfoPlist,"ios.infoPlist");if(isGoogleSignInEnabled&&resolvedReversedClientId){mergeUrlSchemes(plistObject,[{name:"googleSignIn",schemes:[resolvedReversedClientId]}]);mergeQuerySchemes(plistObject,["google",resolvedReversedClientId]);}mergeUrlSchemes(plistObject,pluginUrlSchemes);mergeQuerySchemes(plistObject,pluginQuerySchemes);writePlistObject(plistPath,plistObject);});}catch(err){progress.fail("config",err);process.exit(1);}}async function updateEntitlements(pluginComposition={}){try{const entitlementsPath=path.join(PROJECT_DIR,PROJECT_NAME,`${PROJECT_NAME}.entitlements`);if(!fs.existsSync(entitlementsPath)){return;}restoreManagedFileFromBaseline(entitlementsPath);const entitlementsObject=readPlistObject(entitlementsPath);mergeIntoTopLevelObject(entitlementsObject,pluginComposition.entitlements||{},"ios.entitlements");writePlistObject(entitlementsPath,entitlementsObject);}catch(error){progress.fail("config",error);process.exit(1);}}async function removePluginResourcesFromXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch(err){if(err.code==="ENOENT")return;throw err;}const patterns=[/\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ \*\/ = \{isa = PBXFileReference;[^\n]*path = [^;]*PluginResources\/[^;]+; sourceTree = "<group>"; \};\n/g,/\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ in Resources \*\/ = \{isa = PBXBuildFile;[^\n]*\};\n/g,/\t\t\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ \*\/,\n/g,/\t\t\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ in Resources \*\/,\n/g];let modified=false;for(const pattern of patterns){const nextContent=projectContent.replace(pattern,"");if(nextContent!==projectContent){projectContent=nextContent;modified=true;}}if(modified){fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Removed managed plugin resources from Xcode project","info");}}catch(error){throw new Error(`Could not remove plugin resources from Xcode project: ${error.message}`);}}async function addPluginResourcesToXcodeProject(resources){try{if(resources.length===0){return;}const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch(err){if(err.code==="ENOENT"){throw new Error(`Xcode project file not found at: ${projectFilePath}`);}throw err;}for(const resource of resources){const label=resource.bundleRelativePath;const fileRefId=generateProjectObjectId(label,"_ref");const buildFileId=generateProjectObjectId(label,"_build");const pbxprojPath=formatPbxprojPath(label);const fileType=detectPbxprojFileType(label);const fileRefEntry=`\t\t${fileRefId} /* ${label} */ = {isa = PBXFileReference; lastKnownFileType = ${fileType}; path = ${pbxprojPath}; sourceTree = "<group>"; };`;projectContent=replaceRequired(projectContent,/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`,"PBXFileReference section not found while registering plugin resources");const buildFileEntry=`\t\t${buildFileId} /* ${label} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${label} */; };`;projectContent=replaceRequired(projectContent,/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`,"PBXBuildFile section not found while registering plugin resources");const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;projectContent=replaceRequired(projectContent,groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${label} */,`,"iosnativeWebView group not found while registering plugin resources");const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;projectContent=replaceRequired(projectContent,resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${label} in Resources */,`,"Resources build phase not found while registering plugin resources");}fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log(`Registered ${resources.length} managed plugin resource(s) in Xcode project`,"success");}catch(error){throw new Error(`Could not add plugin resources to Xcode project: ${error.message}`);}}async function syncPluginResources(pluginComposition={}){try{const resources=pluginComposition.resources||[];const pluginResourceDir=path.join(PROJECT_DIR,PROJECT_NAME,PLUGIN_RESOURCE_ROOT);fs.rmSync(pluginResourceDir,{recursive:true,force:true});await removePluginResourcesFromXcodeProject();if(resources.length===0){progress.log("No managed plugin resources to sync","info");return;}for(const resource of resources){const normalizedBundleRelativePath=path.posix.normalize(resource.bundleRelativePath||"");const expectedPrefix=`${PLUGIN_RESOURCE_ROOT}/`;if(!normalizedBundleRelativePath.startsWith(expectedPrefix)||normalizedBundleRelativePath.includes("../")||path.posix.isAbsolute(normalizedBundleRelativePath)){throw new Error(`Invalid managed plugin resource path: ${resource.bundleRelativePath}`);}
|
|
63
|
+
projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/ = {isa = PBXBuildFile; productRef = [A-F0-9]+ \/\* CatalystNotifications \*\/; };\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = [A-F0-9]+ \/\* XCLocalSwiftPackageReference "." \*\/;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n/g,"");fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications removed from Xcode project","success");}else{progress.log("Package dependencies already correct","info");}}catch(error){progress.log(`❌ Failed to update package dependencies: ${error.message}`,"error");throw error;}}async function updateInfoPlist(pluginComposition={}){try{const infoPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info.plist");const infoReleasePlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info-Release.plist");const googleServicesPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,GOOGLE_SERVICES_FILENAME);const googleClientId=WEBVIEW_CONFIG.googleSignIn?.clientId||WEBVIEW_CONFIG.googleSignIn?.webClientId||"";const iosClientId=WEBVIEW_CONFIG.googleSignIn?.iosClientId||"";const googleServicesContent=fs.existsSync(googleServicesPlistPath)?fs.readFileSync(googleServicesPlistPath,"utf8"):null;const reversedClientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>REVERSED_CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const clientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const computeReversed=value=>{if(!value)return"";return value.split(".").reverse().join(".");};const resolvedClientIdForScheme=iosClientId||googleClientId||clientIdFromServices||"";const resolvedReversedClientId=reversedClientIdFromServices||computeReversed(resolvedClientIdForScheme);if(isGoogleSignInEnabled&&!resolvedReversedClientId){progress.fail("config","Google Sign-In enabled but no valid clientId found");process.exit(1);}const plistTargets=[infoPlistPath,infoReleasePlistPath];const pluginUrlSchemes=pluginComposition.urlSchemes||[];const pluginQuerySchemes=pluginComposition.querySchemes||[];const pluginInfoPlist=pluginComposition.infoPlist||{};plistTargets.forEach(plistPath=>{if(!fs.existsSync(plistPath)){return;}restoreManagedFileFromBaseline(plistPath);const plistObject=readPlistObject(plistPath);plistObject.CFBundleDisplayName=iosConfig.appName||"Catalyst Application";mergeIntoTopLevelObject(plistObject,pluginInfoPlist,"ios.infoPlist");if(isGoogleSignInEnabled&&resolvedReversedClientId){mergeUrlSchemes(plistObject,[{name:"googleSignIn",schemes:[resolvedReversedClientId]}]);mergeQuerySchemes(plistObject,["google",resolvedReversedClientId]);}mergeUrlSchemes(plistObject,pluginUrlSchemes);mergeQuerySchemes(plistObject,pluginQuerySchemes);writePlistObject(plistPath,plistObject);});}catch(err){progress.fail("config",err);process.exit(1);}}async function updateEntitlements(pluginComposition={}){try{const entitlementsPath=path.join(PROJECT_DIR,PROJECT_NAME,`${PROJECT_NAME}.entitlements`);if(!fs.existsSync(entitlementsPath)){return;}restoreManagedFileFromBaseline(entitlementsPath);const entitlementsObject=readPlistObject(entitlementsPath);mergeIntoTopLevelObject(entitlementsObject,pluginComposition.entitlements||{},"ios.entitlements");writePlistObject(entitlementsPath,entitlementsObject);}catch(error){progress.fail("config",error);process.exit(1);}}async function removePluginResourcesFromXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch(err){if(err.code==="ENOENT")return;throw err;}const patterns=[/\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ \*\/ = \{isa = PBXFileReference;[^\n]*path = [^;]*PluginResources\/[^;]+; sourceTree = "<group>"; \};\n/g,/\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ in Resources \*\/ = \{isa = PBXBuildFile;[^\n]*\};\n/g,/\t\t\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ \*\/,\n/g,/\t\t\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ in Resources \*\/,\n/g];let modified=false;for(const pattern of patterns){const nextContent=projectContent.replace(pattern,"");if(nextContent!==projectContent){projectContent=nextContent;modified=true;}}if(modified){fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Removed managed plugin resources from Xcode project","info");}}catch(error){throw new Error(`Could not remove plugin resources from Xcode project: ${error.message}`);}}async function addPluginResourcesToXcodeProject(resources){try{if(resources.length===0){return;}const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch(err){if(err.code==="ENOENT"){throw new Error(`Xcode project file not found at: ${projectFilePath}`);}throw err;}for(const resource of resources){const label=resource.bundleRelativePath;const fileRefId=generateProjectObjectId(label,"_ref");const buildFileId=generateProjectObjectId(label,"_build");const pbxprojPath=formatPbxprojPath(label);const fileType=detectPbxprojFileType(label);const fileRefEntry=`\t\t${fileRefId} /* ${label} */ = {isa = PBXFileReference; lastKnownFileType = ${fileType}; path = ${pbxprojPath}; sourceTree = "<group>"; };`;projectContent=replaceRequired(projectContent,/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`,"PBXFileReference section not found while registering plugin resources");const buildFileEntry=`\t\t${buildFileId} /* ${label} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${label} */; };`;projectContent=replaceRequired(projectContent,/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`,"PBXBuildFile section not found while registering plugin resources");const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;projectContent=replaceRequired(projectContent,groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${label} */,`,"iosnativeWebView group not found while registering plugin resources");const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;projectContent=replaceRequired(projectContent,resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${label} in Resources */,`,"Resources build phase not found while registering plugin resources");}fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log(`Registered ${resources.length} managed plugin resource(s) in Xcode project`,"success");}catch(error){throw new Error(`Could not add plugin resources to Xcode project: ${error.message}`);}}async function syncPluginResources(pluginComposition={}){try{const resources=pluginComposition.resources||[];const pluginResourceDir=path.join(PROJECT_DIR,PROJECT_NAME,PLUGIN_RESOURCE_ROOT);fs.rmSync(pluginResourceDir,{recursive:true,force:true});await removePluginResourcesFromXcodeProject();if(resources.length===0){progress.log("No managed plugin resources to sync","info");return;}for(const resource of resources){const normalizedBundleRelativePath=path.posix.normalize(resource.bundleRelativePath||"");const expectedPrefix=`${PLUGIN_RESOURCE_ROOT}/`;if(!normalizedBundleRelativePath.startsWith(expectedPrefix)||normalizedBundleRelativePath.includes("../")||path.posix.isAbsolute(normalizedBundleRelativePath)){throw new Error(`Invalid managed plugin resource path: ${resource.bundleRelativePath}`);}// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The normalized path is validated above and containment is enforced immediately below.
|
|
64
|
+
const targetPath=path.resolve(PROJECT_DIR,PROJECT_NAME,normalizedBundleRelativePath);if(targetPath!==pluginResourceDir&&!targetPath.startsWith(`${pluginResourceDir}${path.sep}`)){throw new Error(`Managed plugin resource escaped bundle directory: ${resource.bundleRelativePath}`);}fs.mkdirSync(path.dirname(targetPath),{recursive:true});fs.copyFileSync(resource.sourcePath,targetPath);}await addPluginResourcesToXcodeProject(resources);progress.log(`Synced ${resources.length} managed plugin resource(s)`,"success");}catch(error){progress.log(`❌ Failed to sync plugin resources: ${error.message}`,"error");throw error;}}// Function to convert JSON value to Swift property
|
|
63
65
|
function generateSwiftProperty(key,value,indent=" "){if(value===null||value===undefined){return`${indent}public static let ${key}: String? = nil`;}// Special handling for cachePattern - always convert to array
|
|
64
66
|
if(key==="cachePattern"){if(typeof value==="string"){// Convert single string to array
|
|
65
67
|
return`${indent}public static let ${key}: [String] = ["${value}"]`;}else if(Array.isArray(value)){const arrayValues=value.map(v=>`"${v}"`).join(", ");return`${indent}public static let ${key}: [String] = [${arrayValues}]`;}}if(typeof value==="string"){return`${indent}public static let ${key} = "${value}"`;}if(typeof value==="number"){return Number.isInteger(value)?`${indent}public static let ${key} = ${value}`:`${indent}public static let ${key} = ${value}`;}if(typeof value==="boolean"){return`${indent}public static let ${key} = ${value}`;}if(Array.isArray(value)){if(value.length===0){return`${indent}public static let ${key}: [String] = []`;}// Determine array type from first element
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function readStringArray(value,fieldName,sourcePath,{required=false,nonEmpty=false}={}){if(!Array.isArray(value)){if(required){throw new Error(`'${fieldName}' is required and must be an array in ${sourcePath}`);}return[];}const result=value.map(entry=>mustBeNonEmptyString(entry,`${fieldName}[]`,sourcePath));if(nonEmpty&&result.length===0){throw new Error(`'${fieldName}' is required and must be non-empty in ${sourcePath}`);}return result;}function readPlainObject(value,fieldName,sourcePath){if(value==null){return{};}if(!value||typeof value!=="object"||Array.isArray(value)){throw new Error(`'${fieldName}' must be an object in ${sourcePath}`);}return value;}function cloneJsonValue(value,fieldName,sourcePath){try{return JSON.parse(JSON.stringify(value));}catch(error){throw new Error(`'${fieldName}' must be JSON-serializable in ${sourcePath}`);}}function readIosUrlSchemes(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}return{name:entry.name==null?null:mustBeNonEmptyString(entry.name,`${entryField}.name`,sourcePath),schemes:readStringArray(entry.schemes,`${entryField}.schemes`,sourcePath,{required:true,nonEmpty:true})};});}function readIosDependencies(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}const url=mustBeNonEmptyString(entry.url,`${entryField}.url`,sourcePath);const packageIdentity=entry.package==null?derivePackageIdentityFromUrl(url,`${entryField}.url`,sourcePath):mustBeNonEmptyString(entry.package,`${entryField}.package`,sourcePath);const products=readStringArray(entry.products,`${entryField}.products`,sourcePath,{required:true,nonEmpty:true});if(new Set(products).size!==products.length){throw new Error(`Duplicate product(s) found in '${entryField}.products' in ${sourcePath}`);}const hasFrom=entry.from!=null;const hasExact=entry.exact!=null;if(hasFrom===hasExact){throw new Error(`'${entryField}' must define exactly one version requirement: 'from' or 'exact' in ${sourcePath}`);}return{url,package:packageIdentity,products,requirement:hasFrom?{type:"from",version:mustBeNonEmptyString(entry.from,`${entryField}.from`,sourcePath)}:{type:"exact",version:mustBeNonEmptyString(entry.exact,`${entryField}.exact`,sourcePath)}};});}function derivePackageIdentityFromUrl(url,fieldName,sourcePath){const sanitizedUrl=url.replace(/\/+$/,"");const packageIdentity=sanitizedUrl.split("/").pop()?.replace(/\.git$/,"");if(!packageIdentity){throw new Error(`Unable to derive package identity from '${fieldName}' in ${sourcePath}`);}return packageIdentity;}function parsePluginManifest(pluginDir){
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function readStringArray(value,fieldName,sourcePath,{required=false,nonEmpty=false}={}){if(!Array.isArray(value)){if(required){throw new Error(`'${fieldName}' is required and must be an array in ${sourcePath}`);}return[];}const result=value.map(entry=>mustBeNonEmptyString(entry,`${fieldName}[]`,sourcePath));if(nonEmpty&&result.length===0){throw new Error(`'${fieldName}' is required and must be non-empty in ${sourcePath}`);}return result;}function readPlainObject(value,fieldName,sourcePath){if(value==null){return{};}if(!value||typeof value!=="object"||Array.isArray(value)){throw new Error(`'${fieldName}' must be an object in ${sourcePath}`);}return value;}function cloneJsonValue(value,fieldName,sourcePath){try{return JSON.parse(JSON.stringify(value));}catch(error){throw new Error(`'${fieldName}' must be JSON-serializable in ${sourcePath}`);}}function readIosUrlSchemes(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}return{name:entry.name==null?null:mustBeNonEmptyString(entry.name,`${entryField}.name`,sourcePath),schemes:readStringArray(entry.schemes,`${entryField}.schemes`,sourcePath,{required:true,nonEmpty:true})};});}function readIosDependencies(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}const url=mustBeNonEmptyString(entry.url,`${entryField}.url`,sourcePath);const packageIdentity=entry.package==null?derivePackageIdentityFromUrl(url,`${entryField}.url`,sourcePath):mustBeNonEmptyString(entry.package,`${entryField}.package`,sourcePath);const products=readStringArray(entry.products,`${entryField}.products`,sourcePath,{required:true,nonEmpty:true});if(new Set(products).size!==products.length){throw new Error(`Duplicate product(s) found in '${entryField}.products' in ${sourcePath}`);}const hasFrom=entry.from!=null;const hasExact=entry.exact!=null;if(hasFrom===hasExact){throw new Error(`'${entryField}' must define exactly one version requirement: 'from' or 'exact' in ${sourcePath}`);}return{url,package:packageIdentity,products,requirement:hasFrom?{type:"from",version:mustBeNonEmptyString(entry.from,`${entryField}.from`,sourcePath)}:{type:"exact",version:mustBeNonEmptyString(entry.exact,`${entryField}.exact`,sourcePath)}};});}function derivePackageIdentityFromUrl(url,fieldName,sourcePath){const sanitizedUrl=url.replace(/\/+$/,"");const packageIdentity=sanitizedUrl.split("/").pop()?.replace(/\.git$/,"");if(!packageIdentity){throw new Error(`Unable to derive package identity from '${fieldName}' in ${sourcePath}`);}return packageIdentity;}function parsePluginManifest(pluginDir){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the filename is fixed.
|
|
2
|
+
const manifestPath=path.join(pluginDir,"manifest.json");if(!fs.existsSync(manifestPath)){return null;}let manifestContent;try{manifestContent=fs.readFileSync(manifestPath,"utf8");}catch(error){throw new Error(`Failed to read plugin manifest at ${manifestPath}: ${error.message}`);}let manifest;try{manifest=JSON.parse(manifestContent);}catch(error){throw new Error(`Invalid JSON in plugin manifest ${manifestPath}: ${error.message}`);}const id=mustBeNonEmptyString(manifest.id,"id",manifestPath);const configKey=mustBeNonEmptyString(manifest.configKey,"configKey",manifestPath);const platforms=readStringArray(manifest.platforms,"platforms",manifestPath,{required:true,nonEmpty:true});const androidConfig=platforms.includes("android")?manifest.android:null;const iosConfig=platforms.includes("ios")?manifest.ios:null;if(platforms.includes("android")&&(!androidConfig||typeof androidConfig!=="object")){throw new Error(`'android' config is required for plugin '${id}' in ${manifestPath}`);}if(platforms.includes("ios")&&(!iosConfig||typeof iosConfig!=="object")){throw new Error(`'ios' config is required for plugin '${id}' in ${manifestPath}`);}return{pluginDir,manifestPath,id,configKey,version:mustBeNonEmptyString(manifest.version,"version",manifestPath),displayName:mustBeNonEmptyString(manifest.displayName,"displayName",manifestPath),description:mustBeNonEmptyString(manifest.description,"description",manifestPath),category:mustBeNonEmptyString(manifest.category,"category",manifestPath),platforms,commands:readStringArray(manifest.commands,"commands",manifestPath,{required:true,nonEmpty:true}),android:androidConfig?{// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the child directory is fixed.
|
|
3
|
+
sourceDir:path.join(pluginDir,"android"),permissions:readStringArray(androidConfig.permissions,"android.permissions",manifestPath),dependencies:readStringArray(androidConfig.dependencies,"android.dependencies",manifestPath),className:mustBeNonEmptyString(androidConfig.className,"android.className",manifestPath)}:null,ios:iosConfig?{// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir is discovered from the internal plugin root and the child directory is fixed.
|
|
4
|
+
sourceDir:path.join(pluginDir,"ios"),dependencies:readIosDependencies(iosConfig.dependencies,"ios.dependencies",manifestPath),className:mustBeNonEmptyString(iosConfig.className,"ios.className",manifestPath),infoPlist:cloneJsonValue(readPlainObject(iosConfig.infoPlist,"ios.infoPlist",manifestPath),"ios.infoPlist",manifestPath),urlSchemes:readIosUrlSchemes(iosConfig.urlSchemes,"ios.urlSchemes",manifestPath),querySchemes:readStringArray(iosConfig.querySchemes,"ios.querySchemes",manifestPath),entitlements:cloneJsonValue(readPlainObject(iosConfig.entitlements,"ios.entitlements",manifestPath),"ios.entitlements",manifestPath),resources:readStringArray(iosConfig.resources,"ios.resources",manifestPath)}:null};}function discoverInternalPlugins(corePluginsRoot,log=()=>{}){if(!corePluginsRoot||!isDir(corePluginsRoot)){log(`No internal plugin directory found at ${corePluginsRoot||"<empty>"}`,"info");return[];}const plugins=[];const entries=fs.readdirSync(corePluginsRoot,{withFileTypes:true}).sort((left,right)=>left.name.localeCompare(right.name));for(const entry of entries){if(!entry.isDirectory()){continue;}// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from readdirSync under the internal plugin root.
|
|
5
|
+
const pluginDir=path.join(corePluginsRoot,entry.name);const parsed=parsePluginManifest(pluginDir);if(parsed){plugins.push(parsed);}}log(`Discovered ${plugins.length} internal plugin manifest(s)`,"info");return plugins;}function resolveInternalPluginsRoot(packageRoot){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed package-internal directories.
|
|
6
|
+
const distPluginsPath=path.join(packageRoot,"dist","native","internal-plugins");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed package-internal directories.
|
|
7
|
+
const srcPluginsPath=path.join(packageRoot,"src","native","internal-plugins");return fs.existsSync(distPluginsPath)?distPluginsPath:srcPluginsPath;}function resolvePluginConfig(WEBVIEW_CONFIG){const pluginConfig={};if(WEBVIEW_CONFIG.plugins!=null){if(typeof WEBVIEW_CONFIG.plugins!=="object"||Array.isArray(WEBVIEW_CONFIG.plugins)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}for(const[key,value]of Object.entries(WEBVIEW_CONFIG.plugins)){if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${key}' must be boolean`);}pluginConfig[key]=value;}}return pluginConfig;}module.exports={discoverInternalPlugins,parsePluginManifest,resolvePluginConfig,resolveInternalPluginsRoot};
|
|
@@ -1,9 +1,28 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");const{discoverInternalPlugins}=require("./internalPluginUtils.js");const PLUGINS_PACKAGE_PARTS=["io","yourname","androidproject","plugins"];function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function ensureDir(dirPath){if(!fs.existsSync(dirPath)){fs.mkdirSync(dirPath,{recursive:true});}}function sanitizeForPath(value){return value.replace(/[^a-zA-Z0-9_-]/g,"_");}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function asUniqueSorted(values){return[...new Set(values)].sort();}function parsePluginToggleConfig(pluginConfig){if(pluginConfig==null){return{};}if(typeof pluginConfig!=="object"||Array.isArray(pluginConfig)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}const toggles={};for(const[key,value]of Object.entries(pluginConfig)){const normalizedKey=mustBeNonEmptyString(key,"plugins.<key>","WEBVIEW_CONFIG");if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${normalizedKey}' must be boolean`);}toggles[normalizedKey]=value;}return toggles;}function selectPluginsByConfig(plugins,pluginConfig,log){const toggles=parsePluginToggleConfig(pluginConfig);const matchedKeys=new Set();const selected=[];for(const plugin of plugins){const selectorKeys=plugin.configKey?[plugin.configKey,plugin.id]:[plugin.id];const matches=[];for(const key of selectorKeys){if(Object.prototype.hasOwnProperty.call(toggles,key)){matches.push({key,value:toggles[key]});matchedKeys.add(key);}}const uniqueValues=[...new Set(matches.map(entry=>entry.value))];if(uniqueValues.length>1){throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry=>`${entry.key}=${entry.value}`).join(", ")}`);}const enabled=matches.length===0?false:matches[0].value;if(enabled){selected.push(plugin);}else{log(`Plugin disabled by config: ${plugin.id}`,"info");}}const unknownKeys=Object.keys(toggles).filter(key=>!matchedKeys.has(key));if(unknownKeys.length>0){throw new Error(`Unknown plugin toggle key(s) in WEBVIEW_CONFIG.plugins: ${unknownKeys.join(", ")}`);}return selected;}function parseDependency(dependency,pluginId){const parts=dependency.split(":");if(parts.length<3){throw new Error(`Dependency '${dependency}' in plugin '${pluginId}' must be in 'group:artifact:version' format`);}return{key:`${parts[0]}:${parts[1]}`,version:parts.slice(2).join(":")};}function validatePlugins(plugins){const pluginIds=new Set();const configKeys=new Set();const dependencies=new Map();const selectorKeys=new Map();for(const plugin of plugins){if(pluginIds.has(plugin.id)){throw new Error(`Duplicate plugin id detected: ${plugin.id}`);}pluginIds.add(plugin.id);if(plugin.configKey){if(configKeys.has(plugin.configKey)){throw new Error(`Duplicate configKey detected: ${plugin.configKey}`);}configKeys.add(plugin.configKey);}for(const[field,selector]of[["id",plugin.id],["configKey",plugin.configKey]]){if(!selector){continue;}const existing=selectorKeys.get(selector);if(existing&&existing.pluginId!==plugin.id){throw new Error(`Plugin selector collision for '${selector}': '${existing.pluginId}' (${existing.field}) conflicts with '${plugin.id}' (${field})`);}selectorKeys.set(selector,{pluginId:plugin.id,field});}if(new Set(plugin.commands).size!==plugin.commands.length){throw new Error(`Duplicate command(s) detected within plugin '${plugin.id}'`);}for(const dependency of plugin.android?.dependencies||[]){const parsed=parseDependency(dependency,plugin.id);const existing=dependencies.get(parsed.key);if(existing&&existing.version!==parsed.version){throw new Error(`Dependency version conflict for '${parsed.key}': '${existing.version}' in '${existing.pluginId}', '${parsed.version}' in '${plugin.id}'`);}if(!existing){dependencies.set(parsed.key,{version:parsed.version,pluginId:plugin.id});}}}}function selectPluginsForPlatform(plugins,platform,log){const selected=[];for(const plugin of plugins){if(plugin.platforms.includes(platform)){selected.push(plugin);continue;}log(`Plugin enabled but not supported on ${platform}: ${plugin.id}`,"info");}return selected;}function walkFiles(rootDir,predicate,results=[]){if(!isDir(rootDir)){return results;}for(const entry of fs.readdirSync(rootDir,{withFileTypes:true})){
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const{discoverInternalPlugins}=require("./internalPluginUtils.js");const PLUGINS_PACKAGE_PARTS=["io","yourname","androidproject","plugins"];function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function ensureDir(dirPath){if(!fs.existsSync(dirPath)){fs.mkdirSync(dirPath,{recursive:true});}}function sanitizeForPath(value){return value.replace(/[^a-zA-Z0-9_-]/g,"_");}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function asUniqueSorted(values){return[...new Set(values)].sort();}function parsePluginToggleConfig(pluginConfig){if(pluginConfig==null){return{};}if(typeof pluginConfig!=="object"||Array.isArray(pluginConfig)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}const toggles={};for(const[key,value]of Object.entries(pluginConfig)){const normalizedKey=mustBeNonEmptyString(key,"plugins.<key>","WEBVIEW_CONFIG");if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${normalizedKey}' must be boolean`);}toggles[normalizedKey]=value;}return toggles;}function selectPluginsByConfig(plugins,pluginConfig,log){const toggles=parsePluginToggleConfig(pluginConfig);const matchedKeys=new Set();const selected=[];for(const plugin of plugins){const selectorKeys=plugin.configKey?[plugin.configKey,plugin.id]:[plugin.id];const matches=[];for(const key of selectorKeys){if(Object.prototype.hasOwnProperty.call(toggles,key)){matches.push({key,value:toggles[key]});matchedKeys.add(key);}}const uniqueValues=[...new Set(matches.map(entry=>entry.value))];if(uniqueValues.length>1){throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry=>`${entry.key}=${entry.value}`).join(", ")}`);}const enabled=matches.length===0?false:matches[0].value;if(enabled){selected.push(plugin);}else{log(`Plugin disabled by config: ${plugin.id}`,"info");}}const unknownKeys=Object.keys(toggles).filter(key=>!matchedKeys.has(key));if(unknownKeys.length>0){throw new Error(`Unknown plugin toggle key(s) in WEBVIEW_CONFIG.plugins: ${unknownKeys.join(", ")}`);}return selected;}function parseDependency(dependency,pluginId){const parts=dependency.split(":");if(parts.length<3){throw new Error(`Dependency '${dependency}' in plugin '${pluginId}' must be in 'group:artifact:version' format`);}return{key:`${parts[0]}:${parts[1]}`,version:parts.slice(2).join(":")};}function validatePlugins(plugins){const pluginIds=new Set();const configKeys=new Set();const dependencies=new Map();const selectorKeys=new Map();for(const plugin of plugins){if(pluginIds.has(plugin.id)){throw new Error(`Duplicate plugin id detected: ${plugin.id}`);}pluginIds.add(plugin.id);if(plugin.configKey){if(configKeys.has(plugin.configKey)){throw new Error(`Duplicate configKey detected: ${plugin.configKey}`);}configKeys.add(plugin.configKey);}for(const[field,selector]of[["id",plugin.id],["configKey",plugin.configKey]]){if(!selector){continue;}const existing=selectorKeys.get(selector);if(existing&&existing.pluginId!==plugin.id){throw new Error(`Plugin selector collision for '${selector}': '${existing.pluginId}' (${existing.field}) conflicts with '${plugin.id}' (${field})`);}selectorKeys.set(selector,{pluginId:plugin.id,field});}if(new Set(plugin.commands).size!==plugin.commands.length){throw new Error(`Duplicate command(s) detected within plugin '${plugin.id}'`);}for(const dependency of plugin.android?.dependencies||[]){const parsed=parseDependency(dependency,plugin.id);const existing=dependencies.get(parsed.key);if(existing&&existing.version!==parsed.version){throw new Error(`Dependency version conflict for '${parsed.key}': '${existing.version}' in '${existing.pluginId}', '${parsed.version}' in '${plugin.id}'`);}if(!existing){dependencies.set(parsed.key,{version:parsed.version,pluginId:plugin.id});}}}}function selectPluginsForPlatform(plugins,platform,log){const selected=[];for(const plugin of plugins){if(plugin.platforms.includes(platform)){selected.push(plugin);continue;}log(`Plugin enabled but not supported on ${platform}: ${plugin.id}`,"info");}return selected;}function walkFiles(rootDir,predicate,results=[]){if(!isDir(rootDir)){return results;}for(const entry of fs.readdirSync(rootDir,{withFileTypes:true})){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from readdirSync within the walked root.
|
|
2
|
+
const fullPath=path.join(rootDir,entry.name);if(entry.isDirectory()){walkFiles(fullPath,predicate,results);continue;}if(predicate(entry.name,fullPath)){results.push(fullPath);}}return results;}function resolvePluginClassSourcePath(plugin){const className=plugin.android.className.split(".").pop();const candidateNames=new Set([`${className}.kt`,`${className}.java`]);const candidates=walkFiles(plugin.android.sourceDir,name=>candidateNames.has(name));if(candidates.length>1){throw new Error(`Declared class '${plugin.android.className}' for selected plugin '${plugin.id}' resolved to multiple source files under ${plugin.android.sourceDir}`);}return candidates[0]||null;}function validateSelectedPluginSources(plugins){for(const plugin of plugins){if(!plugin.android){throw new Error(`Android config missing for selected plugin '${plugin.id}'`);}if(!isDir(plugin.android.sourceDir)){throw new Error(`Android source directory missing for selected plugin '${plugin.id}'`);}const codeFiles=walkFiles(plugin.android.sourceDir,name=>name.endsWith(".kt")||name.endsWith(".java"));if(codeFiles.length===0){throw new Error(`No Android source files found for selected plugin '${plugin.id}'`);}if(!resolvePluginClassSourcePath(plugin)){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}`);}}}function copyTree(sourceDir,targetDir){if(!isDir(sourceDir)){return;}ensureDir(targetDir);for(const filePath of walkFiles(sourceDir,()=>true)){// 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.
|
|
3
|
+
const targetPath=path.join(targetDir,path.relative(sourceDir,filePath));ensureDir(path.dirname(targetPath));fs.copyFileSync(filePath,targetPath);}}function copyAndroidPluginSources(plugins,javaRoot,log){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The package path components are fixed constants.
|
|
4
|
+
const internalRoot=path.join(javaRoot,...PLUGINS_PACKAGE_PARTS,"internal");fs.rmSync(internalRoot,{recursive:true,force:true});ensureDir(internalRoot);let copiedCount=0;for(const plugin of plugins){// 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.
|
|
5
|
+
const pluginOutputDir=path.join(internalRoot,sanitizeForPath(plugin.id));const codeFiles=walkFiles(plugin.android.sourceDir,name=>name.endsWith(".kt")||name.endsWith(".java"));for(const sourcePath of codeFiles){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - sourcePath is produced by walking plugin.android.sourceDir.
|
|
6
|
+
const targetPath=path.join(pluginOutputDir,path.relative(plugin.android.sourceDir,sourcePath));ensureDir(path.dirname(targetPath));fs.copyFileSync(sourcePath,targetPath);copiedCount++;}}log(`Copied ${copiedCount} Android plugin source file(s)`,"info");}function copyPluginAssets(plugins,androidProjectPath,log){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The destination path components are fixed.
|
|
7
|
+
const baseAssetsDir=path.join(androidProjectPath,"app","src","main","assets","plugins");fs.rmSync(baseAssetsDir,{recursive:true,force:true});ensureDir(baseAssetsDir);for(const plugin of plugins){// 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.
|
|
8
|
+
const pluginAssetsDir=path.join(baseAssetsDir,sanitizeForPath(plugin.id));// 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.
|
|
9
|
+
copyTree(path.join(plugin.pluginDir,"assets","common"),path.join(pluginAssetsDir,"common"));// 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.
|
|
10
|
+
copyTree(path.join(plugin.pluginDir,"assets","android"),path.join(pluginAssetsDir,"android"));}log("Plugin assets copied to app/src/main/assets/plugins","info");}function formatKotlinMap(entries,emptyLiteral="emptyMap()"){return entries.length===0?emptyLiteral:`mapOf(\n${entries.join(",\n")}\n )`;}function generatePluginRegistryFiles(plugins,javaRoot){const pluginIdToClassName={};const pluginToCommands={};for(const plugin of plugins){pluginIdToClassName[plugin.id]=plugin.android.className;pluginToCommands[plugin.id]=asUniqueSorted(plugin.commands);}const classEntries=Object.keys(pluginIdToClassName).sort().map(pluginId=>` ${JSON.stringify(pluginId)} to ${JSON.stringify(pluginIdToClassName[pluginId])}`);const commandEntries=Object.keys(pluginToCommands).sort().map(pluginId=>{const commands=pluginToCommands[pluginId];const commandSet=commands.length?`setOf(${commands.map(value=>JSON.stringify(value)).join(", ")})`:"emptySet()";return` ${JSON.stringify(pluginId)} to ${commandSet}`;});const indexContent=`package io.yourname.androidproject.plugins
|
|
2
11
|
|
|
3
12
|
object GeneratedPluginIndex {
|
|
4
13
|
val pluginIdToClassName: Map<String, String> = ${formatKotlinMap(classEntries)}
|
|
5
14
|
val pluginToCommands: Map<String, Set<String>> = ${formatKotlinMap(commandEntries)}
|
|
6
15
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
`;// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The package path components are fixed constants.
|
|
17
|
+
const pluginsDir=path.join(javaRoot,...PLUGINS_PACKAGE_PARTS);ensureDir(pluginsDir);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename is fixed.
|
|
18
|
+
fs.writeFileSync(path.join(pluginsDir,"GeneratedPluginIndex.kt"),indexContent);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename is fixed.
|
|
19
|
+
fs.rmSync(path.join(pluginsDir,"GeneratedPluginMeta.kt"),{force:true});}function escapeRegexLiteral(value){return value.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");}function updateAndroidManifestPermissions(manifestPath,selectedPermissions,allKnownPluginPermissions){const uniquePermissions=asUniqueSorted(selectedPermissions);const knownPermissions=asUniqueSorted(allKnownPluginPermissions);let manifest=fs.readFileSync(manifestPath,"utf8");const beginMarker="<!-- CATALYST_PLUGIN_PERMISSIONS_START -->";const endMarker="<!-- CATALYST_PLUGIN_PERMISSIONS_END -->";const markerRegex=/[ \t]*<!-- CATALYST_PLUGIN_PERMISSIONS_START -->[\s\S]*?<!-- CATALYST_PLUGIN_PERMISSIONS_END -->\s*/g;manifest=manifest.replace(markerRegex,"");// Migration cleanup for legacy entries previously written without markers.
|
|
20
|
+
for(const permission of knownPermissions){const escaped=escapeRegexLiteral(permission);// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - The dynamic value is escaped as a regex literal above.
|
|
21
|
+
const legacyRegex=new RegExp(`^[ \\t]*<uses-permission\\s+android:name="${escaped}"\\s*/>\\s*\\n?`,"gm");manifest=manifest.replace(legacyRegex,"");}if(uniquePermissions.length>0){const permissionLines=uniquePermissions.map(permission=>` <uses-permission android:name="${permission}" />`).join("\n");const managedBlock=` ${beginMarker}\n${permissionLines}\n ${endMarker}\n`;manifest=manifest.replace(/<application\b/,`${managedBlock} <application`);}fs.writeFileSync(manifestPath,manifest);}function findDependenciesBlockRange(gradleText,gradlePath){const headerMatch=gradleText.match(/^dependencies\s*\{/m);if(!headerMatch||headerMatch.index==null){throw new Error(`Could not find dependencies block in ${gradlePath}`);}const openBraceIndex=gradleText.indexOf("{",headerMatch.index);if(openBraceIndex===-1){throw new Error(`Malformed dependencies block in ${gradlePath}`);}let depth=0;for(let index=openBraceIndex;index<gradleText.length;index++){const ch=gradleText[index];if(ch==="{")depth++;if(ch==="}")depth--;if(depth===0){return{openBraceIndex,blockEnd:index};}}throw new Error(`Could not find end of dependencies block in ${gradlePath}`);}function updateGradleDependencies(gradlePath,selectedDependencies,allKnownPluginDependencies){const uniqueDependencies=asUniqueSorted(selectedDependencies);const knownDependencies=asUniqueSorted(allKnownPluginDependencies);let gradle=fs.readFileSync(gradlePath,"utf8");const{openBraceIndex,blockEnd}=findDependenciesBlockRange(gradle,gradlePath);let blockBody=gradle.slice(openBraceIndex+1,blockEnd);const beginMarker="// CATALYST_PLUGIN_DEPENDENCIES_START";const endMarker="// CATALYST_PLUGIN_DEPENDENCIES_END";const markerRegex=/[ \t]*\/\/ CATALYST_PLUGIN_DEPENDENCIES_START[\s\S]*?\/\/ CATALYST_PLUGIN_DEPENDENCIES_END\s*/g;blockBody=blockBody.replace(markerRegex,"");// Migration cleanup for legacy entries previously written without markers.
|
|
22
|
+
for(const dependency of knownDependencies){const escaped=escapeRegexLiteral(dependency);// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - The dynamic value is escaped as a regex literal above.
|
|
23
|
+
const legacyRegex=new RegExp(`^[ \\t]*implementation\\("${escaped}"\\)\\s*\\n?`,"gm");blockBody=blockBody.replace(legacyRegex,"");}if(uniqueDependencies.length>0){const managedLines=uniqueDependencies.map(dependency=>` implementation("${dependency}")`).join("\n");blockBody=`${blockBody}\n ${beginMarker}\n${managedLines}\n ${endMarker}\n`;}gradle=`${gradle.slice(0,openBraceIndex+1)}${blockBody}${gradle.slice(blockEnd)}`;fs.writeFileSync(gradlePath,gradle);}function updateProguardKeepRules(proguardPath,selectedClassNames,allKnownPluginClassNames){const uniqueClassNames=asUniqueSorted(selectedClassNames);const knownClassNames=asUniqueSorted(allKnownPluginClassNames);let rules=fs.readFileSync(proguardPath,"utf8");const beginMarker="# CATALYST_PLUGIN_KEEP_START";const endMarker="# CATALYST_PLUGIN_KEEP_END";const markerRegex=/[ \t]*# CATALYST_PLUGIN_KEEP_START[\s\S]*?# CATALYST_PLUGIN_KEEP_END\s*/g;rules=rules.replace(markerRegex,"");for(const className of knownClassNames){const escaped=escapeRegexLiteral(className);// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - The dynamic value is escaped as a regex literal above.
|
|
24
|
+
const legacyRegex=new RegExp(`^[ \\t]*-keep class ${escaped} \\{ \\*; \\}\\s*\\n?`,"gm");rules=rules.replace(legacyRegex,"");}if(uniqueClassNames.length>0){const keepLines=uniqueClassNames.map(className=>`-keep class ${className} { *; }`).join("\n");rules=`${rules.trimEnd()}\n\n${beginMarker}\n${keepLines}\n${endMarker}\n`;}fs.writeFileSync(proguardPath,rules);}function composeAndroidPlugins({corePluginsRoot,androidProjectPath,pluginConfig,log}){const discovered=discoverInternalPlugins(corePluginsRoot,log);validatePlugins(discovered);const enabled=selectPluginsByConfig(discovered,pluginConfig,log);const selected=selectPluginsForPlatform(enabled,"android",log);validateSelectedPluginSources(selected);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed paths within the generated Android project.
|
|
25
|
+
const javaRoot=path.join(androidProjectPath,"app","src","main","java");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed paths within the generated Android project.
|
|
26
|
+
const manifestPath=path.join(androidProjectPath,"app","src","main","AndroidManifest.xml");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed paths within the generated Android project.
|
|
27
|
+
const gradlePath=path.join(androidProjectPath,"app","build.gradle.kts");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - These are fixed paths within the generated Android project.
|
|
28
|
+
const proguardPath=path.join(androidProjectPath,"app","proguard-rules.pro");copyAndroidPluginSources(selected,javaRoot,log);copyPluginAssets(selected,androidProjectPath,log);generatePluginRegistryFiles(selected,javaRoot);updateAndroidManifestPermissions(manifestPath,selected.flatMap(plugin=>plugin.android?.permissions||[]),discovered.flatMap(plugin=>plugin.android?.permissions||[]));updateGradleDependencies(gradlePath,selected.flatMap(plugin=>plugin.android?.dependencies||[]),discovered.flatMap(plugin=>plugin.android?.dependencies||[]));updateProguardKeepRules(proguardPath,selected.map(plugin=>plugin.android?.className).filter(Boolean),discovered.map(plugin=>plugin.android?.className).filter(Boolean));log(`Plugin composition complete (${selected.length} enabled plugin(s))`,"success");return{pluginCount:selected.length,commandCount:selected.reduce((total,plugin)=>total+plugin.commands.length,0)};}module.exports={composeAndroidPlugins};
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");const{discoverInternalPlugins}=require("./internalPluginUtils.js");function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function ensureDir(dirPath){if(!fs.existsSync(dirPath)){fs.mkdirSync(dirPath,{recursive:true});}}function sanitizeForPath(value){return value.replace(/[^a-zA-Z0-9_-]/g,"_");}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function asUniqueSorted(values){return[...new Set(values)].sort();}function isPlainObject(value){return!!value&&typeof value==="object"&&!Array.isArray(value);}function deepEqual(left,right){return JSON.stringify(left)===JSON.stringify(right);}function mergeStructuredValues(existing,incoming,fieldName){if(existing===undefined){return JSON.parse(JSON.stringify(incoming));}if(Array.isArray(existing)&&Array.isArray(incoming)){const merged=[];const seen=new Set();for(const value of[...existing,...incoming]){const key=JSON.stringify(value);if(seen.has(key)){continue;}seen.add(key);merged.push(value);}return merged;}if(isPlainObject(existing)&&isPlainObject(incoming)){const merged={...existing};for(const[key,value]of Object.entries(incoming)){merged[key]=mergeStructuredValues(merged[key],value,`${fieldName}.${key}`);}return merged;}if(deepEqual(existing,incoming)){return existing;}throw new Error(`Conflicting values for '${fieldName}' while composing selected iOS plugins`);}function dependencyKey(dependency){return dependency.url;}function requirementKey(dependency){return`${dependency.requirement.type}:${dependency.requirement.version}`;}function packageKey(dependency){return dependency.package;}function resolveManifestPath(pluginDir,relativePath,fieldName){const resolvedPath=path.resolve(pluginDir,relativePath);const normalizedPluginDir=fs.realpathSync(path.resolve(pluginDir));if(resolvedPath!==normalizedPluginDir&&!resolvedPath.startsWith(`${normalizedPluginDir}${path.sep}`)){throw new Error(`'${fieldName}' must stay within plugin directory: ${relativePath}`);}if(!fs.existsSync(resolvedPath)){return resolvedPath;}const realResolvedPath=fs.realpathSync(resolvedPath);if(realResolvedPath!==normalizedPluginDir&&!realResolvedPath.startsWith(`${normalizedPluginDir}${path.sep}`)){throw new Error(`'${fieldName}' resolves outside plugin directory: ${relativePath}`);}return realResolvedPath;}function parsePluginToggleConfig(pluginConfig){if(pluginConfig==null){return{};}if(typeof pluginConfig!=="object"||Array.isArray(pluginConfig)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}const toggles={};for(const[key,value]of Object.entries(pluginConfig)){const normalizedKey=mustBeNonEmptyString(key,"plugins.<key>","WEBVIEW_CONFIG");if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${normalizedKey}' must be boolean`);}toggles[normalizedKey]=value;}return toggles;}function selectPluginsByConfig(plugins,pluginConfig,log){const toggles=parsePluginToggleConfig(pluginConfig);const matchedKeys=new Set();const enabled=[];for(const plugin of plugins){const selectorKeys=plugin.configKey?[plugin.configKey,plugin.id]:[plugin.id];const matches=[];for(const key of selectorKeys){if(Object.prototype.hasOwnProperty.call(toggles,key)){matches.push({key,value:toggles[key]});matchedKeys.add(key);}}const uniqueValues=[...new Set(matches.map(entry=>entry.value))];if(uniqueValues.length>1){throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry=>`${entry.key}=${entry.value}`).join(", ")}`);}const isEnabled=matches.length===0?false:matches[0].value;if(isEnabled){enabled.push(plugin);}else{log(`Plugin disabled by config: ${plugin.id}`,"info");}}const unknownKeys=Object.keys(toggles).filter(key=>!matchedKeys.has(key));if(unknownKeys.length>0){throw new Error(`Unknown plugin toggle key(s) in WEBVIEW_CONFIG.plugins: ${unknownKeys.join(", ")}`);}return enabled;}function validatePlugins(plugins){const pluginIds=new Set();const configKeys=new Set();const selectorKeys=new Map();const dependencies=new Map();for(const plugin of plugins){if(pluginIds.has(plugin.id)){throw new Error(`Duplicate plugin id detected: ${plugin.id}`);}pluginIds.add(plugin.id);if(plugin.configKey){if(configKeys.has(plugin.configKey)){throw new Error(`Duplicate configKey detected: ${plugin.configKey}`);}configKeys.add(plugin.configKey);}for(const[field,selector]of[["id",plugin.id],["configKey",plugin.configKey]]){if(!selector){continue;}const existing=selectorKeys.get(selector);if(existing&&existing.pluginId!==plugin.id){throw new Error(`Plugin selector collision for '${selector}': '${existing.pluginId}' (${existing.field}) conflicts with '${plugin.id}' (${field})`);}selectorKeys.set(selector,{pluginId:plugin.id,field});}if(new Set(plugin.commands).size!==plugin.commands.length){throw new Error(`Duplicate command(s) detected within plugin '${plugin.id}'`);}if(Object.prototype.hasOwnProperty.call(plugin.ios?.infoPlist||{},"CFBundleURLTypes")){throw new Error(`Plugin '${plugin.id}' must use 'ios.urlSchemes' instead of 'ios.infoPlist.CFBundleURLTypes'`);}if(Object.prototype.hasOwnProperty.call(plugin.ios?.infoPlist||{},"LSApplicationQueriesSchemes")){throw new Error(`Plugin '${plugin.id}' must use 'ios.querySchemes' instead of 'ios.infoPlist.LSApplicationQueriesSchemes'`);}for(const dependency of plugin.ios?.dependencies||[]){const existing=dependencies.get(dependencyKey(dependency));if(existing&&requirementKey(existing.dependency)!==requirementKey(dependency)){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}'`);}if(existing&&packageKey(existing.dependency)!==packageKey(dependency)){throw new Error(`iOS dependency package identity conflict for '${dependency.url}': '${existing.dependency.package}' in '${existing.pluginId}', '${dependency.package}' in '${plugin.id}'`);}if(!existing){dependencies.set(dependencyKey(dependency),{dependency,pluginId:plugin.id});}}for(const resourcePath of plugin.ios?.resources||[]){const resolvedPath=resolveManifestPath(plugin.pluginDir,resourcePath,`ios.resources for '${plugin.id}'`);if(!fs.existsSync(resolvedPath)){throw new Error(`iOS resource path not found for plugin '${plugin.id}': ${resourcePath}`);}}}}function selectPluginsForPlatform(plugins,platform,log){const selected=[];for(const plugin of plugins){if(plugin.platforms.includes(platform)){selected.push(plugin);continue;}log(`Plugin enabled but not supported on ${platform}: ${plugin.id}`,"info");}return selected;}function walkFiles(rootDir,predicate,results=[]){if(!isDir(rootDir)){return results;}for(const entry of fs.readdirSync(rootDir,{withFileTypes:true})){const fullPath=path.join(rootDir,entry.name);if(entry.isDirectory()){walkFiles(fullPath,predicate,results);continue;}if(predicate(entry.name,fullPath)){results.push(fullPath);}}return results;}function resolvePluginClassSourcePath(plugin){const className=plugin.ios.className.split(".").pop();const candidateName=`${className}.swift`;const candidates=walkFiles(plugin.ios.sourceDir,name=>name===candidateName);return candidates[0]||null;}function validateSelectedPluginSources(plugins){for(const plugin of plugins){if(!plugin.ios){throw new Error(`iOS config missing for selected plugin '${plugin.id}'`);}if(!isDir(plugin.ios.sourceDir)){throw new Error(`iOS source directory missing for selected plugin '${plugin.id}'`);}const codeFiles=walkFiles(plugin.ios.sourceDir,name=>name.endsWith(".swift"));if(codeFiles.length===0){throw new Error(`No iOS source files found for selected plugin '${plugin.id}'`);}if(!resolvePluginClassSourcePath(plugin)){throw new Error(`Declared class '${plugin.ios.className}' for selected plugin '${plugin.id}' was not found under ${plugin.ios.sourceDir}`);}}}function collectIosDependencies(plugins){const dependenciesByUrl=new Map();for(const plugin of plugins){for(const dependency of plugin.ios?.dependencies||[]){const existing=dependenciesByUrl.get(dependency.url);if(!existing){dependenciesByUrl.set(dependency.url,{url:dependency.url,package:dependency.package,requirement:dependency.requirement,products:[...dependency.products]});continue;}if(requirementKey(existing)!==requirementKey(dependency)){throw new Error(`iOS dependency version conflict for '${dependency.url}' while composing selected plugins`);}if(packageKey(existing)!==packageKey(dependency)){throw new Error(`iOS dependency package identity conflict for '${dependency.url}' while composing selected plugins`);}existing.products=asUniqueSorted([...existing.products,...dependency.products]);}}return[...dependenciesByUrl.values()].sort((left,right)=>left.url.localeCompare(right.url));}function normalizeResourceRelativePath(plugin,absolutePath){const relativePath=path.relative(plugin.pluginDir,absolutePath);if(relativePath.startsWith(`ios${path.sep}resources${path.sep}`)){return relativePath.slice(`ios${path.sep}resources${path.sep}`.length);}if(relativePath.startsWith(`ios${path.sep}`)){return relativePath.slice(`ios${path.sep}`.length);}return relativePath;}function validateBundleRelativePath(pluginId,bundleRelativePath){const normalizedPath=path.posix.normalize(bundleRelativePath);const expectedPrefix=`PluginResources/${sanitizeForPath(pluginId)}`;if(path.posix.isAbsolute(normalizedPath)||normalizedPath===".."||normalizedPath.startsWith("../")){throw new Error(`Invalid bundled resource path for plugin '${pluginId}': ${bundleRelativePath}`);}if(normalizedPath!==expectedPrefix&&!normalizedPath.startsWith(`${expectedPrefix}/`)){throw new Error(`Bundled resource path escaped managed directory for plugin '${pluginId}': ${bundleRelativePath}`);}return normalizedPath;}function collectIosResources(plugins){const resources=[];for(const plugin of plugins){for(const resourcePath of plugin.ios?.resources||[]){const resolvedPath=resolveManifestPath(plugin.pluginDir,resourcePath,`ios.resources for '${plugin.id}'`);const entries=fs.statSync(resolvedPath).isDirectory()?walkFiles(resolvedPath,()=>true):[resolvedPath];for(const entryPath of entries){const normalizedRelativePath=normalizeResourceRelativePath(plugin,entryPath);const bundleRelativePath=validateBundleRelativePath(plugin.id,path.join("PluginResources",sanitizeForPath(plugin.id),normalizedRelativePath).split(path.sep).join("/"));resources.push({pluginId:plugin.id,sourcePath:entryPath,bundleRelativePath});}}}return resources.sort((left,right)=>left.bundleRelativePath.localeCompare(right.bundleRelativePath));}function collectIosInfoPlist(plugins){let infoPlist={};for(const plugin of plugins){infoPlist=mergeStructuredValues(infoPlist,plugin.ios?.infoPlist||{},`ios.infoPlist for '${plugin.id}'`);}return infoPlist;}function collectIosEntitlements(plugins){let entitlements={};for(const plugin of plugins){entitlements=mergeStructuredValues(entitlements,plugin.ios?.entitlements||{},`ios.entitlements for '${plugin.id}'`);}return entitlements;}function collectIosUrlSchemes(plugins){const entries=[];for(const plugin of plugins){for(const entry of plugin.ios?.urlSchemes||[]){entries.push({name:entry.name||plugin.id,schemes:asUniqueSorted(entry.schemes)});}}return entries;}function collectIosQuerySchemes(plugins){return asUniqueSorted(plugins.flatMap(plugin=>plugin.ios?.querySchemes||[]));}function copyIosPluginSources(plugins,iosProjectPath,log){const internalRoot=path.join(iosProjectPath,"Sources","Core","Plugins","Internal");fs.rmSync(internalRoot,{recursive:true,force:true});ensureDir(internalRoot);let copiedCount=0;for(const plugin of plugins){const pluginOutputDir=path.join(internalRoot,sanitizeForPath(plugin.id));const codeFiles=walkFiles(plugin.ios.sourceDir,name=>name.endsWith(".swift"));for(const sourcePath of codeFiles){const targetPath=path.join(pluginOutputDir,path.relative(plugin.ios.sourceDir,sourcePath));ensureDir(path.dirname(targetPath));fs.copyFileSync(sourcePath,targetPath);copiedCount++;}}log(`Copied ${copiedCount} iOS plugin source file(s)`,"info");}function formatSwiftDictionary(entries,emptyLiteral="[:]"){return entries.length===0?emptyLiteral:`[\n${entries.join(",\n")}\n ]`;}function generatePluginRegistryFiles(plugins,iosProjectPath){const pluginFactories={};const pluginToCommands={};for(const plugin of plugins){pluginFactories[plugin.id]=plugin.ios.className;pluginToCommands[plugin.id]=asUniqueSorted(plugin.commands);}const factoryEntries=Object.keys(pluginFactories).sort().map(pluginId=>` ${JSON.stringify(pluginId)}: { ${pluginFactories[pluginId]}() }`);const commandEntries=Object.keys(pluginToCommands).sort().map(pluginId=>{const commands=pluginToCommands[pluginId];const commandSet=commands.length?`Set([${commands.map(value=>JSON.stringify(value)).join(", ")}])`:"[]";return` ${JSON.stringify(pluginId)}: ${commandSet}`;});const indexContent=`import Foundation
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const{discoverInternalPlugins}=require("./internalPluginUtils.js");function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function ensureDir(dirPath){if(!fs.existsSync(dirPath)){fs.mkdirSync(dirPath,{recursive:true});}}function sanitizeForPath(value){return value.replace(/[^a-zA-Z0-9_-]/g,"_");}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function asUniqueSorted(values){return[...new Set(values)].sort();}function isPlainObject(value){return!!value&&typeof value==="object"&&!Array.isArray(value);}function deepEqual(left,right){return JSON.stringify(left)===JSON.stringify(right);}function mergeStructuredValues(existing,incoming,fieldName){if(existing===undefined){return JSON.parse(JSON.stringify(incoming));}if(Array.isArray(existing)&&Array.isArray(incoming)){const merged=[];const seen=new Set();for(const value of[...existing,...incoming]){const key=JSON.stringify(value);if(seen.has(key)){continue;}seen.add(key);merged.push(value);}return merged;}if(isPlainObject(existing)&&isPlainObject(incoming)){const merged={...existing};for(const[key,value]of Object.entries(incoming)){merged[key]=mergeStructuredValues(merged[key],value,`${fieldName}.${key}`);}return merged;}if(deepEqual(existing,incoming)){return existing;}throw new Error(`Conflicting values for '${fieldName}' while composing selected iOS plugins`);}function dependencyKey(dependency){return dependency.url;}function requirementKey(dependency){return`${dependency.requirement.type}:${dependency.requirement.version}`;}function packageKey(dependency){return dependency.package;}function resolveManifestPath(pluginDir,relativePath,fieldName){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Containment and realpath checks are enforced immediately below.
|
|
2
|
+
const resolvedPath=path.resolve(pluginDir,relativePath);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pluginDir comes from the internal plugin root.
|
|
3
|
+
const normalizedPluginDir=fs.realpathSync(path.resolve(pluginDir));if(resolvedPath!==normalizedPluginDir&&!resolvedPath.startsWith(`${normalizedPluginDir}${path.sep}`)){throw new Error(`'${fieldName}' must stay within plugin directory: ${relativePath}`);}if(!fs.existsSync(resolvedPath)){return resolvedPath;}const realResolvedPath=fs.realpathSync(resolvedPath);if(realResolvedPath!==normalizedPluginDir&&!realResolvedPath.startsWith(`${normalizedPluginDir}${path.sep}`)){throw new Error(`'${fieldName}' resolves outside plugin directory: ${relativePath}`);}return realResolvedPath;}function parsePluginToggleConfig(pluginConfig){if(pluginConfig==null){return{};}if(typeof pluginConfig!=="object"||Array.isArray(pluginConfig)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}const toggles={};for(const[key,value]of Object.entries(pluginConfig)){const normalizedKey=mustBeNonEmptyString(key,"plugins.<key>","WEBVIEW_CONFIG");if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${normalizedKey}' must be boolean`);}toggles[normalizedKey]=value;}return toggles;}function selectPluginsByConfig(plugins,pluginConfig,log){const toggles=parsePluginToggleConfig(pluginConfig);const matchedKeys=new Set();const enabled=[];for(const plugin of plugins){const selectorKeys=plugin.configKey?[plugin.configKey,plugin.id]:[plugin.id];const matches=[];for(const key of selectorKeys){if(Object.prototype.hasOwnProperty.call(toggles,key)){matches.push({key,value:toggles[key]});matchedKeys.add(key);}}const uniqueValues=[...new Set(matches.map(entry=>entry.value))];if(uniqueValues.length>1){throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry=>`${entry.key}=${entry.value}`).join(", ")}`);}const isEnabled=matches.length===0?false:matches[0].value;if(isEnabled){enabled.push(plugin);}else{log(`Plugin disabled by config: ${plugin.id}`,"info");}}const unknownKeys=Object.keys(toggles).filter(key=>!matchedKeys.has(key));if(unknownKeys.length>0){throw new Error(`Unknown plugin toggle key(s) in WEBVIEW_CONFIG.plugins: ${unknownKeys.join(", ")}`);}return enabled;}function validatePlugins(plugins){const pluginIds=new Set();const configKeys=new Set();const selectorKeys=new Map();const dependencies=new Map();for(const plugin of plugins){if(pluginIds.has(plugin.id)){throw new Error(`Duplicate plugin id detected: ${plugin.id}`);}pluginIds.add(plugin.id);if(plugin.configKey){if(configKeys.has(plugin.configKey)){throw new Error(`Duplicate configKey detected: ${plugin.configKey}`);}configKeys.add(plugin.configKey);}for(const[field,selector]of[["id",plugin.id],["configKey",plugin.configKey]]){if(!selector){continue;}const existing=selectorKeys.get(selector);if(existing&&existing.pluginId!==plugin.id){throw new Error(`Plugin selector collision for '${selector}': '${existing.pluginId}' (${existing.field}) conflicts with '${plugin.id}' (${field})`);}selectorKeys.set(selector,{pluginId:plugin.id,field});}if(new Set(plugin.commands).size!==plugin.commands.length){throw new Error(`Duplicate command(s) detected within plugin '${plugin.id}'`);}if(Object.prototype.hasOwnProperty.call(plugin.ios?.infoPlist||{},"CFBundleURLTypes")){throw new Error(`Plugin '${plugin.id}' must use 'ios.urlSchemes' instead of 'ios.infoPlist.CFBundleURLTypes'`);}if(Object.prototype.hasOwnProperty.call(plugin.ios?.infoPlist||{},"LSApplicationQueriesSchemes")){throw new Error(`Plugin '${plugin.id}' must use 'ios.querySchemes' instead of 'ios.infoPlist.LSApplicationQueriesSchemes'`);}for(const dependency of plugin.ios?.dependencies||[]){const existing=dependencies.get(dependencyKey(dependency));if(existing&&requirementKey(existing.dependency)!==requirementKey(dependency)){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}'`);}if(existing&&packageKey(existing.dependency)!==packageKey(dependency)){throw new Error(`iOS dependency package identity conflict for '${dependency.url}': '${existing.dependency.package}' in '${existing.pluginId}', '${dependency.package}' in '${plugin.id}'`);}if(!existing){dependencies.set(dependencyKey(dependency),{dependency,pluginId:plugin.id});}}for(const resourcePath of plugin.ios?.resources||[]){const resolvedPath=resolveManifestPath(plugin.pluginDir,resourcePath,`ios.resources for '${plugin.id}'`);if(!fs.existsSync(resolvedPath)){throw new Error(`iOS resource path not found for plugin '${plugin.id}': ${resourcePath}`);}}}}function selectPluginsForPlatform(plugins,platform,log){const selected=[];for(const plugin of plugins){if(plugin.platforms.includes(platform)){selected.push(plugin);continue;}log(`Plugin enabled but not supported on ${platform}: ${plugin.id}`,"info");}return selected;}function walkFiles(rootDir,predicate,results=[]){if(!isDir(rootDir)){return results;}for(const entry of fs.readdirSync(rootDir,{withFileTypes:true})){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from readdirSync within the walked root.
|
|
4
|
+
const fullPath=path.join(rootDir,entry.name);if(entry.isDirectory()){walkFiles(fullPath,predicate,results);continue;}if(predicate(entry.name,fullPath)){results.push(fullPath);}}return results;}function resolvePluginClassSourcePath(plugin){const className=plugin.ios.className.split(".").pop();const candidateName=`${className}.swift`;const candidates=walkFiles(plugin.ios.sourceDir,name=>name===candidateName);return candidates[0]||null;}function validateSelectedPluginSources(plugins){for(const plugin of plugins){if(!plugin.ios){throw new Error(`iOS config missing for selected plugin '${plugin.id}'`);}if(!isDir(plugin.ios.sourceDir)){throw new Error(`iOS source directory missing for selected plugin '${plugin.id}'`);}const codeFiles=walkFiles(plugin.ios.sourceDir,name=>name.endsWith(".swift"));if(codeFiles.length===0){throw new Error(`No iOS source files found for selected plugin '${plugin.id}'`);}if(!resolvePluginClassSourcePath(plugin)){throw new Error(`Declared class '${plugin.ios.className}' for selected plugin '${plugin.id}' was not found under ${plugin.ios.sourceDir}`);}}}function collectIosDependencies(plugins){const dependenciesByUrl=new Map();for(const plugin of plugins){for(const dependency of plugin.ios?.dependencies||[]){const existing=dependenciesByUrl.get(dependency.url);if(!existing){dependenciesByUrl.set(dependency.url,{url:dependency.url,package:dependency.package,requirement:dependency.requirement,products:[...dependency.products]});continue;}if(requirementKey(existing)!==requirementKey(dependency)){throw new Error(`iOS dependency version conflict for '${dependency.url}' while composing selected plugins`);}if(packageKey(existing)!==packageKey(dependency)){throw new Error(`iOS dependency package identity conflict for '${dependency.url}' while composing selected plugins`);}existing.products=asUniqueSorted([...existing.products,...dependency.products]);}}return[...dependenciesByUrl.values()].sort((left,right)=>left.url.localeCompare(right.url));}function normalizeResourceRelativePath(plugin,absolutePath){const relativePath=path.relative(plugin.pluginDir,absolutePath);if(relativePath.startsWith(`ios${path.sep}resources${path.sep}`)){return relativePath.slice(`ios${path.sep}resources${path.sep}`.length);}if(relativePath.startsWith(`ios${path.sep}`)){return relativePath.slice(`ios${path.sep}`.length);}return relativePath;}function validateBundleRelativePath(pluginId,bundleRelativePath){const normalizedPath=path.posix.normalize(bundleRelativePath);const expectedPrefix=`PluginResources/${sanitizeForPath(pluginId)}`;if(path.posix.isAbsolute(normalizedPath)||normalizedPath===".."||normalizedPath.startsWith("../")){throw new Error(`Invalid bundled resource path for plugin '${pluginId}': ${bundleRelativePath}`);}if(normalizedPath!==expectedPrefix&&!normalizedPath.startsWith(`${expectedPrefix}/`)){throw new Error(`Bundled resource path escaped managed directory for plugin '${pluginId}': ${bundleRelativePath}`);}return normalizedPath;}function collectIosResources(plugins){const resources=[];for(const plugin of plugins){for(const resourcePath of plugin.ios?.resources||[]){const resolvedPath=resolveManifestPath(plugin.pluginDir,resourcePath,`ios.resources for '${plugin.id}'`);const entries=fs.statSync(resolvedPath).isDirectory()?walkFiles(resolvedPath,()=>true):[resolvedPath];for(const entryPath of entries){const normalizedRelativePath=normalizeResourceRelativePath(plugin,entryPath);const candidateBundleRelativePath=path// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The resulting path is checked by validateBundleRelativePath below.
|
|
5
|
+
.join("PluginResources",sanitizeForPath(plugin.id),normalizedRelativePath).split(path.sep).join("/");const bundleRelativePath=validateBundleRelativePath(plugin.id,candidateBundleRelativePath);resources.push({pluginId:plugin.id,sourcePath:entryPath,bundleRelativePath});}}}return resources.sort((left,right)=>left.bundleRelativePath.localeCompare(right.bundleRelativePath));}function collectIosInfoPlist(plugins){let infoPlist={};for(const plugin of plugins){infoPlist=mergeStructuredValues(infoPlist,plugin.ios?.infoPlist||{},`ios.infoPlist for '${plugin.id}'`);}return infoPlist;}function collectIosEntitlements(plugins){let entitlements={};for(const plugin of plugins){entitlements=mergeStructuredValues(entitlements,plugin.ios?.entitlements||{},`ios.entitlements for '${plugin.id}'`);}return entitlements;}function collectIosUrlSchemes(plugins){const entries=[];for(const plugin of plugins){for(const entry of plugin.ios?.urlSchemes||[]){entries.push({name:entry.name||plugin.id,schemes:asUniqueSorted(entry.schemes)});}}return entries;}function collectIosQuerySchemes(plugins){return asUniqueSorted(plugins.flatMap(plugin=>plugin.ios?.querySchemes||[]));}function copyIosPluginSources(plugins,iosProjectPath,log){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The destination path components are fixed.
|
|
6
|
+
const internalRoot=path.join(iosProjectPath,"Sources","Core","Plugins","Internal");fs.rmSync(internalRoot,{recursive:true,force:true});ensureDir(internalRoot);let copiedCount=0;for(const plugin of plugins){// 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.
|
|
7
|
+
const pluginOutputDir=path.join(internalRoot,sanitizeForPath(plugin.id));const codeFiles=walkFiles(plugin.ios.sourceDir,name=>name.endsWith(".swift"));for(const sourcePath of codeFiles){// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - sourcePath is produced by walking plugin.ios.sourceDir.
|
|
8
|
+
const targetPath=path.join(pluginOutputDir,path.relative(plugin.ios.sourceDir,sourcePath));ensureDir(path.dirname(targetPath));fs.copyFileSync(sourcePath,targetPath);copiedCount++;}}log(`Copied ${copiedCount} iOS plugin source file(s)`,"info");}function formatSwiftDictionary(entries,emptyLiteral="[:]"){return entries.length===0?emptyLiteral:`[\n${entries.join(",\n")}\n ]`;}function generatePluginRegistryFiles(plugins,iosProjectPath){const pluginFactories={};const pluginToCommands={};for(const plugin of plugins){pluginFactories[plugin.id]=plugin.ios.className;pluginToCommands[plugin.id]=asUniqueSorted(plugin.commands);}const factoryEntries=Object.keys(pluginFactories).sort().map(pluginId=>` ${JSON.stringify(pluginId)}: { ${pluginFactories[pluginId]}() }`);const commandEntries=Object.keys(pluginToCommands).sort().map(pluginId=>{const commands=pluginToCommands[pluginId];const commandSet=commands.length?`Set([${commands.map(value=>JSON.stringify(value)).join(", ")}])`:"[]";return` ${JSON.stringify(pluginId)}: ${commandSet}`;});const indexContent=`import Foundation
|
|
2
9
|
|
|
3
10
|
enum GeneratedPluginIndex {
|
|
4
11
|
static let pluginFactories: [String: () -> CatalystPlugin] = ${formatSwiftDictionary(factoryEntries)}
|
|
5
12
|
static let pluginToCommands: [String: Set<String>] = ${formatSwiftDictionary(commandEntries)}
|
|
6
13
|
}
|
|
7
|
-
|
|
14
|
+
`;// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The destination path components are fixed.
|
|
15
|
+
const pluginsDir=path.join(iosProjectPath,"Sources","Core","Plugins");ensureDir(pluginsDir);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - The generated filename is fixed.
|
|
16
|
+
fs.writeFileSync(path.join(pluginsDir,"GeneratedPluginIndex.swift"),indexContent);}function composeIosPlugins({corePluginsRoot,iosProjectPath,pluginConfig,log}){const discovered=discoverInternalPlugins(corePluginsRoot,log);validatePlugins(discovered);const enabled=selectPluginsByConfig(discovered,pluginConfig,log);const selected=selectPluginsForPlatform(enabled,"ios",log);validateSelectedPluginSources(selected);const iosDependencies=collectIosDependencies(selected);const infoPlist=collectIosInfoPlist(selected);const urlSchemes=collectIosUrlSchemes(selected);const querySchemes=collectIosQuerySchemes(selected);const entitlements=collectIosEntitlements(selected);const resources=collectIosResources(selected);copyIosPluginSources(selected,iosProjectPath,log);generatePluginRegistryFiles(selected,iosProjectPath);log(`Plugin composition complete (${selected.length} enabled iOS plugin(s))`,"success");return{pluginCount:selected.length,commandCount:selected.reduce((total,plugin)=>total+plugin.commands.length,0),iosDependencies,infoPlist,urlSchemes,querySchemes,entitlements,resources};}module.exports={composeIosPlugins};
|