catalyst-core 0.1.0-beta.4 → 0.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +14 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +19 -12
- package/dist/native/androidSetup.js +3 -2
- package/dist/native/buildAndroid/assets.js +52 -0
- package/dist/native/buildAndroid/build.js +17 -0
- package/dist/native/buildAndroid/config.js +1 -0
- package/dist/native/buildAndroid/index.js +1 -0
- package/dist/native/buildAppAndroid.js +2 -148
- package/dist/native/buildAppIos.js +2 -304
- package/dist/native/buildIos/assets.js +8 -0
- package/dist/native/buildIos/build.js +39 -0
- package/dist/native/buildIos/config.js +71 -0
- package/dist/native/buildIos/index.js +6 -0
- package/dist/native/buildIos/plugins.js +45 -0
- package/dist/native/internalPluginUtils.js +7 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +17 -9
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
- package/dist/native/pluginComposerAndroid.js +23 -4
- package/dist/native/pluginComposerIos.js +11 -2
- package/dist/native/setupEmulatorIos.js +6 -4
- package/mcp_v2/tools/knowledge.js +2 -2
- package/package.json +1 -1
|
@@ -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};
|
|
@@ -3,7 +3,8 @@ const existingConfigFile=_fs.default.readFileSync(configPath,"utf8");const exist
|
|
|
3
3
|
const updatedConfig={...existingConfig,// Preserve all existing keys
|
|
4
4
|
WEBVIEW_CONFIG:{...existingConfig.WEBVIEW_CONFIG,ios:newConfig.WEBVIEW_CONFIG.ios}// Update only WEBVIEW_CONFIG
|
|
5
5
|
};// Save the merged config
|
|
6
|
-
_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}async function setupIOSEnvironment(){try{progress.start("platform");if(process.platform!=="darwin"){progress.log("iOS Simulator is only available on macOS.");process.exit(1);}progress.complete("platform");progress.start("config");const{WEBVIEW_CONFIG}=await initializeConfig();progress.complete("config");progress.start("simulator");await configureSimulator(WEBVIEW_CONFIG);progress.complete("simulator");progress.start("launch");await launchIOSSimulator(WEBVIEW_CONFIG.ios.simulatorName);progress.complete("launch");progress.start("saveConfig");const config=await(0,_utils.validateAndCompleteConfig)("ios",configPath);progress.complete("saveConfig");progress.start("setupServer");await setupServer(configPath);progress.start("setupServer");progress.printTreeContent("Configuration Explanation",["WEBVIEW_CONFIG: Main configuration object for the WebView setup",{text:"port: Port number for the WebView server",indent:1,prefix:"├─ ",color:"gray"},{text:"ios: iOS-specific configuration",indent:1,prefix:"└─ ",color:"gray"},{text:"buildType: Build type (debug/release)",indent:2,prefix:"├─ ",color:"gray"},{text:"appBundleId: iOS application bundle identifier",indent:2,prefix:"├─ ",color:"gray"},{text:"simulatorName: Selected iOS simulator name",indent:2,prefix:"└─ ",color:"gray"}]);progress.printTreeContent("Final Configuration",[JSON.stringify(config,null,2)]);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);}process.exit(1);}}async function displayPaginatedList(items,startIndex){const endIndex=Math.min(startIndex+ITEMS_PER_PAGE,items.length);for(let i=startIndex;i<endIndex;i++){console.log(`${i+1}. ${items[i]}`);}if(endIndex<items.length){console.log(`\nShowing ${startIndex+1}-${endIndex} of ${items.length} items`);console.log('Type "more" to see more items, or enter your selection: ');return true;}return false;}async function getRuntime(){const runtimesOutput=(0,_child_process.execSync)("xcrun simctl list runtimes -j").toString();const parsedRuntimes=JSON.parse(runtimesOutput).runtimes;const availableRuntime=parsedRuntimes.find(runtime=>runtime.isAvailable||runtime.availability==="(available)"||runtime.availability==="available");if(!availableRuntime){const runningSimulators=(0,_child_process.execSync)("xcrun simctl list devices booted").toString();if(runningSimulators.includes("(Booted)")&&parsedRuntimes.length>0){console.log(`Using runtime: ${parsedRuntimes[0].name}`);return parsedRuntimes[0];}console.error("No available iOS runtime found. Please install one through Xcode.");process.exit(1);}console.log(`Using runtime: ${availableRuntime.name}`);return availableRuntime;}async function configureSimulator(config){
|
|
6
|
+
_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}async function setupIOSEnvironment(){try{progress.start("platform");if(process.platform!=="darwin"){progress.log("iOS Simulator is only available on macOS.");process.exit(1);}progress.complete("platform");progress.start("config");const{WEBVIEW_CONFIG}=await initializeConfig();progress.complete("config");progress.start("simulator");await configureSimulator(WEBVIEW_CONFIG);progress.complete("simulator");progress.start("launch");await launchIOSSimulator(WEBVIEW_CONFIG.ios.simulatorName);progress.complete("launch");progress.start("saveConfig");const config=await(0,_utils.validateAndCompleteConfig)("ios",configPath);progress.complete("saveConfig");progress.start("setupServer");await setupServer(configPath);progress.start("setupServer");progress.printTreeContent("Configuration Explanation",["WEBVIEW_CONFIG: Main configuration object for the WebView setup",{text:"port: Port number for the WebView server",indent:1,prefix:"├─ ",color:"gray"},{text:"ios: iOS-specific configuration",indent:1,prefix:"└─ ",color:"gray"},{text:"buildType: Build type (debug/release)",indent:2,prefix:"├─ ",color:"gray"},{text:"appBundleId: iOS application bundle identifier",indent:2,prefix:"├─ ",color:"gray"},{text:"simulatorName: Selected iOS simulator name",indent:2,prefix:"└─ ",color:"gray"}]);progress.printTreeContent("Final Configuration",[JSON.stringify(config,null,2)]);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);}process.exit(1);}}async function displayPaginatedList(items,startIndex){const endIndex=Math.min(startIndex+ITEMS_PER_PAGE,items.length);for(let i=startIndex;i<endIndex;i++){console.log(`${i+1}. ${items[i]}`);}if(endIndex<items.length){console.log(`\nShowing ${startIndex+1}-${endIndex} of ${items.length} items`);console.log('Type "more" to see more items, or enter your selection: ');return true;}return false;}async function getRuntime(){const runtimesOutput=(0,_child_process.execSync)("xcrun simctl list runtimes -j").toString();const parsedRuntimes=JSON.parse(runtimesOutput).runtimes;const availableRuntime=parsedRuntimes.find(runtime=>runtime.isAvailable||runtime.availability==="(available)"||runtime.availability==="available");if(!availableRuntime){const runningSimulators=(0,_child_process.execSync)("xcrun simctl list devices booted").toString();if(runningSimulators.includes("(Booted)")&&parsedRuntimes.length>0){console.log(`Using runtime: ${parsedRuntimes[0].name}`);return parsedRuntimes[0];}console.error("No available iOS runtime found. Please install one through Xcode.");process.exit(1);}console.log(`Using runtime: ${availableRuntime.name}`);return availableRuntime;}async function configureSimulator(config){// If a simulator is already configured, skip the interactive picker
|
|
7
|
+
if(config.ios.simulatorName){progress.log(`Using configured simulator: ${config.ios.simulatorName}`);return;}progress.pause();// Get the current runtime first
|
|
7
8
|
const runtime=await getRuntime();// Get simulators specifically for this runtime
|
|
8
9
|
const simulatorsJson=JSON.parse((0,_child_process.execSync)("xcrun simctl list devices -j").toString());const runtimeSimulators=simulatorsJson.devices[runtime.identifier]||[];// Filter for available simulators only
|
|
9
10
|
const availableSimulators=runtimeSimulators.filter(sim=>sim.availability==="(available)"||sim.isAvailable).map(sim=>`${sim.name} (${sim.udid})`);if(availableSimulators.length===0){progress.log(`No available simulators found for runtime: ${runtime.name}`,"error");progress.resume();process.exit(1);return;}let startIndex=0;console.log(`\nAvailable simulators for ${runtime.name} (or type 'new' to create a new one):`);// eslint-disable-next-line no-constant-condition
|
|
@@ -26,9 +27,10 @@ const simulatorName=selectedSimulator.split(" (")[0];config.ios.simulatorName=si
|
|
|
26
27
|
// }
|
|
27
28
|
// }
|
|
28
29
|
async function launchIOSSimulator(simulatorName){progress.log("Launching iOS Simulator...");try{// Get all simulators including both available and booted ones
|
|
29
|
-
const allSimulatorInfo=(0,_child_process.execSync)("xcrun simctl list devices -j").toString();const simulatorsJson=JSON.parse(allSimulatorInfo);// Search through all runtimes and their devices
|
|
30
|
-
let foundSimulator=null;let foundSimulatorId=null;let isBooted=false;//
|
|
31
|
-
Object.
|
|
30
|
+
const allSimulatorInfo=(0,_child_process.execSync)("xcrun simctl list devices -j").toString();const simulatorsJson=JSON.parse(allSimulatorInfo);const runtimesOutput=(0,_child_process.execSync)("xcrun simctl list runtimes -j").toString();const availableRuntimeIds=new Set(JSON.parse(runtimesOutput).runtimes.filter(r=>r.isAvailable||r.availability==="(available)"||r.availability==="available").map(r=>r.identifier));// Search through all runtimes and their devices
|
|
31
|
+
let foundSimulator=null;let foundSimulatorId=null;let isBooted=false;// Prefer devices under an available runtime; fall back to already-booted ones
|
|
32
|
+
Object.entries(simulatorsJson.devices).forEach(([runtimeId,devices])=>{const isAvailableRuntime=availableRuntimeIds.has(runtimeId);devices.forEach(device=>{if(device.name===simulatorName){const alreadyBooted=device.state==="Booted";// Take this match if: no match yet, or this runtime is available and current isn't, or device is already booted
|
|
33
|
+
if(!foundSimulator||isAvailableRuntime&&!foundSimulator._fromAvailableRuntime||alreadyBooted){foundSimulator={...device,_fromAvailableRuntime:isAvailableRuntime};foundSimulatorId=device.udid;isBooted=alreadyBooted;}}});});if(!foundSimulator){console.log(`Configured simulator "${simulatorName}" not found.`);return;}if(!isBooted){console.log(`Booting simulator: ${simulatorName}`);(0,_utils.runCommand)(`xcrun simctl boot ${foundSimulatorId}`);}else{console.log(`Simulator ${simulatorName} is already booted`);}// Open Simulator.app and focus
|
|
32
34
|
progress.log("Opening Simulator.app...");(0,_utils.runCommand)("open -a Simulator");// Give the simulator a moment to open/focus
|
|
33
35
|
await new Promise(resolve=>setTimeout(resolve,1000));// Activate the Simulator.app window to bring it to front
|
|
34
36
|
(0,_utils.runCommand)("osascript -e 'tell application \"Simulator\" to activate'");console.log("iOS Simulator launched successfully.");}catch(error){console.error("Failed to launch iOS Simulator. Error:",error.message);}// process.exit(0);
|
|
@@ -61,7 +61,7 @@ function fetchRaw(url) {
|
|
|
61
61
|
// v0.1.0, v0.1.0-canary.8, v0.0.3-canary-18, v0.1.0-beta.2
|
|
62
62
|
function sanitizeGithubTag(version) {
|
|
63
63
|
if (!version) return "main"
|
|
64
|
-
return /^v\d+\.\d+\.\d+(-[a-zA-Z0-9]+([
|
|
64
|
+
return /^v\d+\.\d+\.\d+(-[a-zA-Z0-9]+([.-]\d+)?)?$/.test(version) ? version : "main"
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Fetch first available file — used for KB-miss GitHub fallback
|
|
@@ -101,7 +101,7 @@ async function fetchFromGithub(files, installedVersion) {
|
|
|
101
101
|
// Strip all FTS5 special characters before quoting so keywords are treated as literals.
|
|
102
102
|
function buildFtsQuery(keywords) {
|
|
103
103
|
return keywords
|
|
104
|
-
.map((k) => `"${k.replace(/[^a-zA-Z0-9 _
|
|
104
|
+
.map((k) => `"${k.replace(/[^a-zA-Z0-9 _-]/g, " ").trim()}"`)
|
|
105
105
|
.filter((k) => k.length > 2) // drop empty/whitespace-only terms after stripping
|
|
106
106
|
.join(" OR ")
|
|
107
107
|
}
|