UNPKG

@capacitor/cli

Version:

Capacitor: Cross-platform apps with JavaScript and the web

274 lines (273 loc) 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.__testables = exports.migrateToUIScene = void 0; const fs_extra_1 = require("fs-extra"); const path_1 = require("path"); const common_1 = require("../common"); const log_1 = require("../log"); const fs_1 = require("../util/fs"); const spm_1 = require("../util/spm"); const template_1 = require("../util/template"); const xcode_1 = require("../util/xcode"); async function migrateToUIScene(config) { const signals = readDetectionSignals(config); const state = classify(signals); switch (state) { case 'already-migrated': log_1.logger.info('UIScene migration: project already migrated, skipping.'); return; case 'partial': log_1.logger.warn(`UIScene migration: project is in a partial state (${describeSignals(signals)}). ` + `Skipping automated migration — finish the migration by hand or reset to a clean 8.4 state first.`); return; case 'eligible': break; } const assets = await loadTemplateAssets(config); if (!assets) { log_1.logger.error('UIScene migration: could not read shipped iOS template assets; skipping.'); return; } await (0, common_1.runTask)('Adding UIApplicationSceneManifest to Info.plist.', () => (0, spm_1.addSceneManifestIfNeeded)(config)); await (0, common_1.runTask)('Writing SceneDelegate.swift.', async () => { const { written } = writeSceneDelegate(config, assets.sceneDelegate); if (!written) { log_1.logger.warn('SceneDelegate.swift already exists, skipping.'); } }); await (0, common_1.runTask)('Patching AppDelegate.swift with configurationForConnecting.', async () => { const { patched, reason } = patchAppDelegate(config, assets.configurationForConnectingSnippet); if (!patched && reason) { log_1.logger.warn(`AppDelegate.swift not patched: ${reason}`); } }); await (0, common_1.runTask)('Registering SceneDelegate.swift with the Xcode App target.', async () => { var _a; const pbxprojPath = (0, path_1.join)(config.ios.nativeXcodeProjDirAbs, 'project.pbxproj'); try { const { added } = (0, xcode_1.addSwiftFileToAppTarget)(pbxprojPath, 'App', 'SceneDelegate.swift'); if (!added) { log_1.logger.warn('SceneDelegate.swift is already registered in the App target, skipping.'); } } catch (err) { log_1.logger.warn(`Could not register SceneDelegate.swift automatically: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}. ` + 'Add SceneDelegate.swift to the App target in Xcode manually.'); } }); await scanAndWarn(config); printNextSteps(); } exports.migrateToUIScene = migrateToUIScene; async function scanAndWarn(config) { const findings = []; const swiftFiles = await (0, fs_1.readdirp)(config.ios.platformDirAbs, { filter: (item) => { if (!item.stats.isFile()) return false; if (!item.path.endsWith('.swift')) return false; const p = item.path; return (!p.includes(`${path_1.sep}Pods${path_1.sep}`) && !p.includes(`${path_1.sep}build${path_1.sep}`) && !p.includes(`${path_1.sep}DerivedData${path_1.sep}`) && !p.includes(`${path_1.sep}.build${path_1.sep}`)); }, }); const tokenPatterns = [ { token: /UIApplication\.shared\.applicationState/, label: 'UIApplication.shared.applicationState' }, { token: /\btmpWindow\b/, label: 'tmpWindow' }, { token: /\bTmpViewController\b/, label: 'TmpViewController' }, ]; for (const filePath of swiftFiles) { const source = (0, fs_extra_1.readFileSync)(filePath, 'utf-8'); source.split('\n').forEach((line, idx) => { for (const { token, label } of tokenPatterns) { if (token.test(line)) { findings.push(`${filePath}:${idx + 1}: uses ${label}`); } } }); } const appDelegatePath = (0, path_1.join)(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'); if ((0, fs_extra_1.existsSync)(appDelegatePath)) { const source = (0, fs_extra_1.readFileSync)(appDelegatePath, 'utf-8'); if (hasCustomDelegateBody(source, /func application\([^)]*\bopen url:/)) { findings.push(`${appDelegatePath}: custom application(_:open:) body — review for UIScene compatibility`); } if (hasCustomDelegateBody(source, /func application\([^)]*\bcontinue userActivity:/)) { findings.push(`${appDelegatePath}: custom application(_:continue:) body — review for UIScene compatibility`); } } if (findings.length === 0) return; log_1.logger.warn('UIScene scan found patterns that may need manual review:'); for (const finding of findings) { log_1.logger.warn(` ${finding}`); } } function hasCustomDelegateBody(source, sigRegex) { const match = source.match(sigRegex); if (!match || match.index === undefined) return false; const openIdx = source.indexOf('{', match.index); if (openIdx === -1) return false; let depth = 1; let i = openIdx + 1; while (i < source.length && depth > 0) { const ch = source[i]; if (ch === '{') depth++; else if (ch === '}') depth--; i++; } if (depth !== 0) return false; const body = source.slice(openIdx + 1, i - 1); const codeLines = body .split('\n') .map((l) => l.trim()) .filter((l) => l.length > 0 && !l.startsWith('//')); if (codeLines.length === 0) return false; return codeLines.some((l) => !l.includes('ApplicationDelegateProxy.shared')); } function printNextSteps() { log_1.logger.info(''); log_1.logger.info('UIScene migration next steps:'); log_1.logger.info(' • Review any warnings above for legacy API usage or custom AppDelegate URL/activity handlers.'); log_1.logger.info(' • Full guide: https://capacitorjs.com/docs/updating/8-5'); } async function loadTemplateAssets(config) { const packageManager = await config.ios.packageManager; const archiveName = packageManager === 'SPM' ? 'ios-spm-template.tar.gz' : 'ios-pods-template.tar.gz'; const archivePath = (0, path_1.join)(config.cli.assetsDirAbs, archiveName); const tempDir = (0, path_1.join)(config.cli.assetsDirAbs, 'tempUISceneTemplate'); try { await (0, template_1.extractTemplate)(archivePath, tempDir); const sceneDelegatePath = (0, path_1.join)(tempDir, 'App', 'App', 'SceneDelegate.swift'); const appDelegatePath = (0, path_1.join)(tempDir, 'App', 'App', 'AppDelegate.swift'); if (!(0, fs_extra_1.existsSync)(sceneDelegatePath) || !(0, fs_extra_1.existsSync)(appDelegatePath)) { return null; } const sceneDelegate = (0, fs_extra_1.readFileSync)(sceneDelegatePath, 'utf-8'); const appDelegateSource = (0, fs_extra_1.readFileSync)(appDelegatePath, 'utf-8'); const configurationForConnectingSnippet = extractConfigurationForConnecting(appDelegateSource); if (!configurationForConnectingSnippet) { return null; } return { sceneDelegate, configurationForConnectingSnippet }; } finally { (0, fs_1.deleteFolderRecursive)(tempDir); } } function writeSceneDelegate(config, contents) { const path = (0, path_1.join)(config.ios.nativeTargetDirAbs, 'SceneDelegate.swift'); if ((0, fs_extra_1.existsSync)(path)) { return { written: false }; } (0, fs_extra_1.writeFileSync)(path, contents); return { written: true }; } function patchAppDelegate(config, snippet) { const path = (0, path_1.join)(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'); if (!(0, fs_extra_1.existsSync)(path)) { return { patched: false, reason: 'AppDelegate.swift not found.' }; } const source = (0, fs_extra_1.readFileSync)(path, 'utf-8'); if (source.includes('UISceneConfiguration(name:')) { return { patched: false, reason: 'configurationForConnecting already present.' }; } const patched = insertBeforeAppDelegateClassEnd(source, snippet); if (!patched) { return { patched: false, reason: 'could not locate AppDelegate class body.' }; } (0, fs_extra_1.writeFileSync)(path, patched); return { patched: true }; } function extractConfigurationForConnecting(appDelegateSource) { const sigRegex = /^ {4}func application\(_ application: UIApplication,\n {21}configurationForConnecting\b/m; const sigMatch = appDelegateSource.match(sigRegex); if (!sigMatch || sigMatch.index === undefined) { return null; } const openIdx = appDelegateSource.indexOf('{', sigMatch.index); if (openIdx === -1) { return null; } let depth = 1; let i = openIdx + 1; while (i < appDelegateSource.length && depth > 0) { const ch = appDelegateSource[i]; if (ch === '{') depth++; else if (ch === '}') depth--; i++; } if (depth !== 0) { return null; } return '\n' + appDelegateSource.slice(sigMatch.index, i) + '\n'; } function insertBeforeAppDelegateClassEnd(source, snippet) { const classDeclRegex = /\bclass\s+AppDelegate\b[^{]*\{/; const match = source.match(classDeclRegex); if (!match || match.index === undefined) { return null; } const openIdx = source.indexOf('{', match.index); let depth = 1; let i = openIdx + 1; while (i < source.length && depth > 0) { const ch = source[i]; if (ch === '{') depth++; else if (ch === '}') depth--; i++; } if (depth !== 0) { return null; } const closeIdx = i - 1; return source.slice(0, closeIdx) + snippet + source.slice(closeIdx); } function readDetectionSignals(config) { const sceneDelegatePath = (0, path_1.join)(config.ios.nativeTargetDirAbs, 'SceneDelegate.swift'); const appDelegatePath = (0, path_1.join)(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'); return { hasManifest: (0, spm_1.hasSceneManifest)(config), hasSceneDelegate: (0, fs_extra_1.existsSync)(sceneDelegatePath), hasConfigurationForConnecting: (0, fs_extra_1.existsSync)(appDelegatePath) && (0, fs_extra_1.readFileSync)(appDelegatePath, 'utf-8').includes('UISceneConfiguration(name:'), }; } function classify(signals) { const { hasManifest, hasSceneDelegate, hasConfigurationForConnecting } = signals; const trueCount = [hasManifest, hasSceneDelegate, hasConfigurationForConnecting].filter(Boolean).length; if (trueCount === 0) return 'eligible'; if (trueCount === 3) return 'already-migrated'; return 'partial'; } function describeSignals({ hasManifest, hasSceneDelegate, hasConfigurationForConnecting, }) { const present = []; const missing = []; (hasManifest ? present : missing).push('UIApplicationSceneManifest'); (hasSceneDelegate ? present : missing).push('SceneDelegate.swift'); (hasConfigurationForConnecting ? present : missing).push('AppDelegate.configurationForConnecting'); return `present: [${present.join(', ')}]; missing: [${missing.join(', ')}]`; } // Exported for tests. exports.__testables = { classify, describeSignals, insertBeforeAppDelegateClassEnd, extractConfigurationForConnecting, hasCustomDelegateBody, scanAndWarn, };