Skip to content

Commit

Permalink
Added plugin to configure xcode proj when building
Browse files Browse the repository at this point in the history
  • Loading branch information
BreckoEC committed Mar 7, 2023
1 parent d9e0bb7 commit 1a60a91
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 1 deletion.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module.exports = {
'max-len': [2, { code: 120 }],
'@typescript-eslint/no-unused-vars': ['warn', { caughtErrors: 'none' }],
'@typescript-eslint/ban-ts-comment': ['off'],
'@typescript-eslint/no-non-null-assertion': ['off'],
'prettier/prettier': [
'error',
{
Expand Down
1 change: 1 addition & 0 deletions app.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./plugin/build')
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"prettier": "^2.8.1",
"prettier-eslint": "^15.0.1",
"prettier-eslint-cli": "^7.1.0",
"typescript": "^4.7.4"
"typescript": "^4.7.4",
"fs-jetpack": "^5.1.0"
},
"peerDependencies": {
"expo": "*",
Expand Down
36 changes: 36 additions & 0 deletions plugin/assets/ios/ShareExtension-Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>ShareExtension</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>{{CFBundleShortVersionString}}</string>
<key>CFBundleVersion</key>
<string>{{CFBundleVersion}}</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>{{NSExtensionActivationRule}}</string>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>{{NSExtensionMainStoryboard}}</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
10 changes: 10 additions & 0 deletions plugin/assets/ios/ShareExtension.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix){{IDENTIFIER}}</string>
</array>
</dict>
</plist>
252 changes: 252 additions & 0 deletions plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import { ConfigPlugin, withEntitlementsPlist, withXcodeProject, withInfoPlist } from '@expo/config-plugins'
import assert from 'assert'
import * as jetpack from 'fs-jetpack'
import path from 'path'

export type ShareExtensionPluginProps = {
devTeam: string
extensionPath: string
mainStoryboardName: string
activationRule: string | string[]
supportSuggestions?: boolean
overrideDeploymentTarget?: string
overrideSwiftVersion?: string
}

const PLUGIN_PATH = 'share-extension-expo-plugin'
const EXTENSION_TARGET_NAME = 'ShareExtension'

const ENTITLEMENTS_FILENAME = `${EXTENSION_TARGET_NAME}.entitlements`
const INFO_PLIST_FILENAME = `${EXTENSION_TARGET_NAME}-Info.plist`

const DEFAULT_DEPLOYMENT_TARGET = '15.0'
const DEFAULT_SWIFT_VERSION = '5.7'

const REGEX_IDENTIFIER = /{{IDENTIFIER}}/gm
const REGEX_BUNDLE_SHORT_VERSION = /{{CFBundleShortVersionString}}/gm
const REGEX_BUNDLE_VERSION = /{{CFBundleVersion}}/gm
const REGEX_EXTENSION_ACTIVATION_RULE = /{{NSExtensionActivationRule}}/gm
const REGEX_EXTENSION_MAIN_STORYBOARD = /{{NSExtensionMainStoryboard}}/gm

const withShareExtension: ConfigPlugin<ShareExtensionPluginProps> = (config, pluginProps) => {
assert(config.version, 'Missing {expo.version} in app config.')
assert(config.ios?.bundleIdentifier, 'Missing {expo.ios.bundleIdentifier} in app config.')
assert(config.ios?.buildNumber, 'Missing {expo.ios.buildNumber} in app config.')

if (pluginProps.supportSuggestions) {
config = withMessageIntent(config, pluginProps)
}

config = withKeychainSharing(config, pluginProps)
config = withShareExtensionTarget(config, pluginProps)
config = withEasManagedCredentials(config, pluginProps)
return config
}

const withMessageIntent: ConfigPlugin<ShareExtensionPluginProps> = (config) => {
return withInfoPlist(config, (config) => {
if (!Array.isArray(config.modResults.NSUserActivityTypes)) {
config.modResults.NSUserActivityTypes = []
}

if (!config.modResults.NSUserActivityTypes.includes('INSendMessageIntent')) {
config.modResults.NSUserActivityTypes.push('INSendMessageIntent')
}

return config
})
}

const withKeychainSharing: ConfigPlugin<ShareExtensionPluginProps> = (config) => {
return withEntitlementsPlist(config, (config) => {
const KEYCHAIN_ACCESS_GROUP = 'keychain-access-groups'

if (!Array.isArray(config.modResults[KEYCHAIN_ACCESS_GROUP])) {
config.modResults[KEYCHAIN_ACCESS_GROUP] = []
}

const modResultsArray = config.modResults[KEYCHAIN_ACCESS_GROUP] as any[]

const entitlement = `$(AppIdentifierPrefix)${config.ios!.bundleIdentifier}`

if (modResultsArray.indexOf(entitlement) !== -1) {
return config
}

modResultsArray.push(entitlement)
return config
})
}

const withShareExtensionTarget: ConfigPlugin<ShareExtensionPluginProps> = (config, pluginProps) => {
return withXcodeProject(config, (config) => {
let modulesPath = 'node_modules'
for (let x = 0; x < 5 && !jetpack.exists(modulesPath); x++) {
modulesPath = '../' + modulesPath
}
const source = `${modulesPath}/${PLUGIN_PATH}/plugin/assets/ios`
const destination = `${config.modRequest.platformProjectRoot}/${EXTENSION_TARGET_NAME}`
const xcodeProject = config.modResults

const devTeam = pluginProps.devTeam
const deploymentTarget = pluginProps?.overrideDeploymentTarget ?? DEFAULT_DEPLOYMENT_TARGET
const swiftVersion = pluginProps.overrideSwiftVersion ?? DEFAULT_SWIFT_VERSION
const deviceFamily = config.ios?.isTabletOnly ? '"2"' : config.ios?.supportsTablet ? '"1,2"' : '"1"'
const bundleIdentifier = config.ios!.bundleIdentifier!
const bundleVersion = config.ios!.buildNumber!
const bundleShortVersion = config.version!
const mainStoryboardName = pluginProps.mainStoryboardName

let activationRule = ''
if (typeof pluginProps.activationRule === 'string') {
activationRule = pluginProps.activationRule as string
} else {
const rules = pluginProps.activationRule as string[]
activationRule = `SUBQUERY ( \
extensionItems, \
$extensionItem, \
SUBQUERY ( \
$extensionItem.attachments, \
$attachment, \
(${rules
.map((x) => `ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.${x}"`)
.join(' || ')})
).@count == 1 \
).@count == 1`
}

const sourceFiles = jetpack.find(pluginProps.extensionPath, { matching: '*.swift' })
const resourceFiles = jetpack.find(pluginProps.extensionPath, { matching: '*.storyboard' })

const allFiles = [
`${source}/${ENTITLEMENTS_FILENAME}`,
`${source}/${INFO_PLIST_FILENAME}`,
...sourceFiles,
...resourceFiles,
]

// Copy assets into xcode project
jetpack.dir(destination)
allFiles.forEach((x) => jetpack.copy(x, `${destination}/${path.basename(x)}`))

// Fix group identifier in entitlements
const entitlementsPath = `${destination}/${ENTITLEMENTS_FILENAME}`
let entitlementsFile = jetpack.read(entitlementsPath)!
entitlementsFile = entitlementsFile.replace(REGEX_IDENTIFIER, bundleIdentifier)
jetpack.write(entitlementsPath, entitlementsFile)

// Fix missing data in info plist
const infoPlistPath = `${destination}/${ENTITLEMENTS_FILENAME}`
let infoPlistFile = jetpack.read(infoPlistPath)!
infoPlistFile = infoPlistFile.replace(REGEX_BUNDLE_SHORT_VERSION, bundleShortVersion)
infoPlistFile = infoPlistFile.replace(REGEX_BUNDLE_VERSION, bundleVersion)
infoPlistFile = infoPlistFile.replace(REGEX_EXTENSION_MAIN_STORYBOARD, mainStoryboardName)
infoPlistFile = infoPlistFile.replace(REGEX_EXTENSION_ACTIVATION_RULE, activationRule)
jetpack.write(infoPlistPath, infoPlistFile)

// Add assets in an xcode 'PBXGroup' (xcode folders)
const allFilesNames = allFiles.map((x) => path.basename(x))
const group = xcodeProject.addPbxGroup(allFilesNames, EXTENSION_TARGET_NAME, EXTENSION_TARGET_NAME)

// Add the new PBXGroup to the top level group
// This makes the folder appear in the file explorer in Xcode
const groups = xcodeProject.hash.project.objects['PBXGroup']
const rootKey = Object.keys(groups).find(
(key) => !key.includes('comment') && groups[key].name === undefined && groups[key].path !== 'Pods'
)
xcodeProject.addToPbxGroup(group.uuid, rootKey)

// WORK AROUND for xcodeProject.addTarget BUG
// Xcode projects don't contain these if there is only one target
// An upstream fix should be made to the code referenced in this link:
// https://github.com/apache/cordova-node-xcode/blob/8b98cabc5978359db88dc9ff2d4c015cba40f150/lib/pbxProject.js#L860
const projObjects = xcodeProject.hash.project.objects
projObjects['PBXTargetDependency'] = projObjects['PBXTargetDependency'] || {}
projObjects['PBXContainerItemProxy'] = projObjects['PBXTargetDependency'] || {}

if (xcodeProject.pbxTargetByName(EXTENSION_TARGET_NAME)) {
console.log(`${EXTENSION_TARGET_NAME} already exists in project. Skipping...`)
return config
}

// Add the target
// This adds PBXTargetDependency and PBXContainerItemProxy for you
const target = xcodeProject.addTarget(
EXTENSION_TARGET_NAME,
'app_extension',
EXTENSION_TARGET_NAME,
`${bundleIdentifier}.${EXTENSION_TARGET_NAME}`
)

// Add build phases to the new target
xcodeProject.addBuildPhase(
sourceFiles.map((x) => path.basename(x)),
'PBXSourcesBuildPhase',
'Sources',
target.uuid
)
xcodeProject.addBuildPhase(
resourceFiles.map((x) => path.basename(x)),
'PBXResourcesBuildPhase',
'Resources',
target.uuid
)

// Edit build settings
const configurations = xcodeProject.pbxXCBuildConfigurationSection()
for (const key in configurations) {
if (
typeof configurations[key].buildSettings !== 'undefined' &&
configurations[key].buildSettings.PRODUCT_NAME === `"${EXTENSION_TARGET_NAME}"`
) {
const buildSettingsObj = configurations[key].buildSettings
buildSettingsObj.DEVELOPMENT_TEAM = devTeam
buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = deploymentTarget
buildSettingsObj.TARGETED_DEVICE_FAMILY = deviceFamily
buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `${EXTENSION_TARGET_NAME}/${ENTITLEMENTS_FILENAME}`
buildSettingsObj.CODE_SIGN_STYLE = 'Automatic'
buildSettingsObj.SWIFT_VERSION = swiftVersion
}
}

// Add development teams to both your target and the original project
xcodeProject.addTargetAttribute('DevelopmentTeam', devTeam, target)
xcodeProject.addTargetAttribute('DevelopmentTeam', devTeam)

jetpack.write(config.modResults.filepath, xcodeProject.writeSync())

return config
})
}

const withEasManagedCredentials: ConfigPlugin<ShareExtensionPluginProps> = (config) => {
config.extra = {
...config.extra,
eas: {
...config.extra?.eas,
build: {
...config.extra?.eas?.build,
experimental: {
...config.extra?.eas?.build?.experimental,
ios: {
...config.extra?.eas?.build?.experimental?.ios,
appExtensions: [
...(config.extra?.eas?.build?.experimental?.ios?.appExtensions ?? []),
{
targetName: EXTENSION_TARGET_NAME,
bundleIdentifier: `${config.ios!.bundleIdentifier!}.${EXTENSION_TARGET_NAME}`,
entitlements: {
'keychain-access-groups': [`$(AppIdentifierPrefix)${config.ios!.bundleIdentifier!}`],
},
},
],
},
},
},
},
}

return config
}

export default withShareExtension
9 changes: 9 additions & 0 deletions plugin/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "expo-module-scripts/tsconfig.plugin",
"compilerOptions": {
"outDir": "build",
"rootDir": "src"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}
21 changes: 21 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"

brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"

braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
Expand Down Expand Up @@ -3932,6 +3939,13 @@ fs-extra@~8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"

fs-jetpack@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/fs-jetpack/-/fs-jetpack-5.1.0.tgz#dcd34d709b69007c9dc2420a6f2b9e8f986cff0d"
integrity sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==
dependencies:
minimatch "^5.1.0"

fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
Expand Down Expand Up @@ -5179,6 +5193,13 @@ mimic-fn@^1.0.0:
dependencies:
brace-expansion "^1.1.7"

minimatch@^5.1.0:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"

minimist@^1.2.0, minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
Expand Down

0 comments on commit 1a60a91

Please sign in to comment.