Project API
To write custom scripts and code that manage iOS and Android targets in your native projects, install the @trapezedev/project
package:
npm install @trapezedev/project
TypeScript Note
Make sure the dom
lib is enabled in your tsconfig.json
in the lib
entry under compilerOptions
.
Requirements
For Android: java
must be on PATH
or JAVA_HOME
must be set to use Gradle configuration. This is because the Gradle modification functionality uses a Java utility under the hood for accuracy, as Gradle is a Groovy DSL and Groovy is a JVM language. If you have Android Studio installed, you can use the JDK bundled with it. Note: JAVA_HOME
should be set to the root of the JDK installation, not the /bin
folder.
API Usage
To initialize the project, set the config and initialize a new MobileProject
instance:
import { MobileProject, MobileProjectConfig } from '@trapezedev/project';
const config: MobileProjectConfig = {
ios: {
path: 'ios',
},
android: {
path: 'android',
},
};
const project = new MobileProject('project/root', config);
await project.load();
Committing Changes
The API works by updating files in a virtual filesystem, and no changes are actually committed to the filesystem until project.commit()
is called. When your changes are ready to be saved, run
project.commit();
To get a preview of changes that will be committed, the VFS
object can be accessed on the project:
const changedFiles = project.vfs.all();
changedFiles.forEach((f) => {
console.log(f.getFilename(), f.getData());
});
Once the project is loaded, iOS and Android operations can be performed on the project, as shown below:
iOS
iOS Supports multiple targets and build names (i.e. Debug
or Release
).
Targets
For apps that use multiple targets, such as an App Clip or Watch app, operations on the project can be isolated to specific targets and also build names (Debug
or Release
). However, most methods allow you to pass null
as the targetName
which will then default to using the main App target in the app, which is useful for apps that only have one main App target.
Additionally, buildName
can be set to null
in all methods or, if it's the last argument, left out entirely, however the behavior here is less well defined. Sometimes this uses the first build (such as Debug
or Release
), and sometimes it uses both. Documentation on which scenarios occur when is forthcoming.
To get the Targets in the app, use:
// Get all targets in the project
project.ios?.getTargets();
// Targets have properties like id, name, productName, productType, and a list of buildConfigurations
// Get the main app target in the project
const appTarget = project.ios?.getAppTarget();
Project Settings
Bundle ID
// Get the bundle id for the given target, pass the build name as an optional second parameter
project.ios?.getBundleId(appTarget.name);
project.ios?.setBundleId(targetName, buildName, 'io.ionic.betterBundleId');
project.ios?.setBundleId(targetName, null, 'io.ionic.betterBundleId');
Version and Build Number
The version and build number can be managed, including incrementing the build number which is useful for automated builds:
// Get the numeric build number (aka CURRENT_PROJECT_VERSION) for the given target and build name
project.ios?.getBuild(targetName, buildName);
// Get the version name (aka the MARKETING_VERSION)
project.ios?.getVersion(targetName, buildName);
// Set the numeric build number
await project.ios?.setBuild(targetName, buildName, 42);
// Increment the build number
await project.ios?.incrementBuild(targetName, buildName);
// Set the marketing version
await project.ios?.setVersion(targetName, buildName, '1.2.3');
Display Name
The display name can be managed:
project.ios?.getDisplayName(targetName, buildName);
project.ios?.setDisplayName(targetName, buildName, 'Really Awesome App');
Info.plist
Modifications to the Info.plist
for the given target and build can be made by passing in an object corresponding to entries in the plist.
Note: this method will use the registered INFOPLIST_FILE
for the given target and build so make sure that is set correctly if you've renamed the Info.plist
file.
await project.ios?.updateInfoPlist(targetName, buildName, {
NSFaceIDUsageDescription: 'The better to see you with',
});
// Get the relative path to the Info.plist for the target and build
await project.ios?.getInfoPlist(targetName, buildName);
// Get the full path to the Info.plist
await project.ios?.getInfoPlistFilename(targetName, buildName);
Frameworks
Frameworks, Libraries, and Embedded Content can be managed:
project.ios?.addFramework(targetName, 'ImageIO.framework');
// Complex framework setups can pass options. Boolean fields supported:
// embed, link, customFramework
project.ios?.addFramework(targetName, 'Custom.framework', {
embed: true,
});
project.ios?.getFrameworks(targetName);
Entitlements
Entitlements can be managed:
// The key for the key/value entries should be the low-level entitlement key,
// which can be found on the Apple docs: https://developer.apple.com/documentation/bundleresources/entitlements?language=objc
await project.ios?.addEntitlements(targetName, buildName, {
'keychain-access-groups': ['group1', 'group2'],
});
await project.ios?.getEntitlements(targetName);
Build Configurations
Build settings for targets and specific builds can be managed:
// Configurations will be an array of object with fields name and buildSettings which is an object
// containing all build varibles for the build in the target, such as compiler options like
// ENABLE_BITCODE
project.ios?.getBuildConfigurations(target);
// Individual build properties can be read or written:
project.ios?.setBuildProperty(targetName, buildName, 'FAKE_PROPERTY', 'YES');
project.ios?.getBuildProperty(targetName, buildName, 'FAKE_PROPERTY');
JSON files
Use JsonFile
to make modifications against project JSON files:
// Get a JSON file from the iOS project, or create a new one:
const jsonFile = ctx.project.ios?.getProjectFile<JsonFile>(
filename,
(filename: string) => new JsonFile(filename, ctx.project.vfs, ctx.project),
);
// Load it
await jsonFile.load();
// Set values
jsonFile.set({
field: 'value'
});
// Merge values
jsonFile.merge({
field: {
field2: 'value'
}
});
Android
Android functionality currently supported includes making modifications to AndroidManifest.xml
(attributes and new elements), updating package name, updating version name/code, adding resources files, and making Gradle modifications.
Project Settings
Package Name
project.android?.setPackageName('com.ionicframework.awesome');
project.android?.getPackageName();
Version Name and Code
await project.ios?.setVersion('App', 'Debug', '1.4.5');
await project.ios?.incrementBuild('App');
project.ios?.getBuild('App', 'Debug');
project.ios?.getBuild('App', 'Release');
await project.android?.setVersionName('1.0.2');
await project.android?.getVersionName();
await project.android?.setVersionCode(11);
await project.android?.getVersionCode();
await project.android?.incrementVersionCode();
await project.android?.setVersionNameSuffix('beta');
await project.android?.getVersionNameSuffix();
Android Manifest
Attributes can be added/modified on target elements, and new XML fragments can be injected. Note: if an XPath selector returns multiple elements, the operation will be applied to each.
// Set attributes on a target element:
project.android?.getAndroidManifest().setAttrs('manifest/application', {
'android:name': 'com.ionicframework.test.CoolApplication',
});
// Inject fragment at target:
project.android?.getAndroidManifest().injectFragment(
'manifest',
`
<queries>
<package />
<intent>
</intent>
</queries>
`
);
There is also a method for querying the manifest using an XPath selector:
project.android?.getAndroidManifest().find('manifest/application');
Resource Files
Resource files can be created or existing files copied to:
// Add a resource to the given resource directory with the given name and contents
await project.android?.addResource('raw', 'test.json', `{}`);
// Existing files can also be copied, passing the source of the file as the last argument
await project.android?.copyToResources('drawable', 'icon.png', source);
// To load an existing resource:
const data = await project.android?.getResource('raw', 'test.json');
copyToResources
supports a URL as the last source
option which will fetch the remote file instead.
Gradle
Gradle modifications are the most complicated and powerful of the capabilities in this library. Remember, java
must be on PATH
or JAVA_HOME
must be set before using these methods.
Note: this feature only supports Groovy-based Gradle files.
First, get a reference to the GradleFile
from the project. There are two possible options currently supported when referenced from the project: build.gradle
or app/build.gradle
. To modify other Gradle files use GradleFile directly.
const buildGradleFile = project.android?.getGradleFile('build.gradle');
const appBuildGradleFile = project.android?.getGradleFile('app/build.gradle');
Gradle fragment strings can be injected at specific locations in the Gradle file, or new properties can be added using an object syntax.
To add properties:
// The first argument is the target element. This uses a nested syntax where the terminal method/property name should have an empty object. The second element is an array of new gradle properties to insert:
buildGradleFile.insertProperties(
{
buildscript: {},
},
[{ classpath: 'com.my.custom.gradle.plugin' }]
);
// To insert a variable assignment (thing = value) set the insertType to 'variable':
buildGradleFile.insertProperties(
{
buildscript: {},
},
insertType: 'variable',
[{ firebaseMessagingVersion: '20.0.6' }]
);
To add raw Gradle strings:
appBuildGradleFile.insertFragment({
allprojects: {
repositories: {}
}
}, [{
maven: [
{ url: 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' },
{ name: 'Duo-SDK-Feed }
]
}]
JSON files
Use JsonFile
to make modifications against project JSON files:
// Get a JSON file from the iOS project, or create a new one:
const jsonFile = ctx.project.android?.getProjectFile<JsonFile>(
filename,
(filename: string) => new JsonFile(filename, ctx.project.vfs, ctx.project),
);
// Load it
await jsonFile.load();
// Set values
jsonFile.set({
field: 'value'
});
// Merge values
jsonFile.merge({
field: {
field2: 'value'
}
});