The time has come for you to create an add-on package, and you are determined to use Second Generation Packaging. We’ll show you how we did it here.
One of the biggest bonuses when using Second Generation Packaging (2GP) is the ability to develop two packages with dependencies using the same namespace and ‘packaging org’ (actually a Dev Hub here). You can even use the same IDE, and not ever have to worry about namespace prefixing in any of the source code.
We’re going to go through the process while using only the SFDX CLI. Everything set up here works fine in the common IDEs, but I won’t be diving into that here. Let me know if this would be helpful someday.
We’re also going to use the same application that we were developing and upgrading in the previous blog post here. So if you’ve been through that post this should be easy to follow along with. If you’re following from a theoretical standpoint I think you’ll be fine, there’s not a ton going on here.
I’ll be focusing on a scenario with one parent/main package and one child/add on package.
Similar to our previous post regarding ancestry, there are many differences. But with regards to multiple packages the following are the ones to focus on:
sfdx-project.json
file. Each segment below will dive deeper into the differences.Let’s set up the main package in the force-app
directory, just like before. The sfdx-project.json file from the last article looks like this:
{
"packageDirectories": [
{
"path": "force-app",
"default": true,
"package": "curiousancestry",
"versionNumber": "0.5.0.NEXT"
}
],
"namespace": "curiousancestry",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "48.0",
"packageAliases": {
"curiousancestry": "parentPackageId",
"curiousancestry@0.1.0-2": "parentVersion1",
"curiousancestry@0.2.0-1": "parentVersion2",
"curiousancestry@0.3.0-1": "parentVersion3",
"curiousancestry@0.4.0-1": "parentVersion4",
"curiousancestry@0.5.0-1": "parentVersion5"
}
}
Now we’re going to add the child package. Let’s start by creating the directory in your project root called child-app
.
Next, create the package usign sfdx CLI, this will update your sfdx-package.json
file with a new packageDirectory
element.
sfdx force:package:create --name <yourpackagename> --packagetype Managed --path child-app/
{
"packageDirectories": [
{
"path": "force-app",
"default": true,
"package": "curiousancestry",
"versionNumber": "0.5.0.NEXT"
},
{
"path": "child-app",
"default": false,
"package": "childpackage",
"versionNumber": "0.1.0.NEXT"
}
],
"namespace": "curiousancestry",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "48.0",
"packageAliases": {
"curiousancestry": "parentPackageId",
"curiousancestry@0.1.0-2": "parentVersion1",
"curiousancestry@0.2.0-1": "parentVersion2",
"curiousancestry@0.3.0-1": "parentVersion3",
"curiousancestry@0.4.0-1": "parentVersion4",
"curiousancestry@0.5.0-1": "parentVersion5"
}
}
This creates another package in the same namespace (and a new alias), but it doesn’t tie the two packages together just yet. Let’s add the dependencies collection. Here’s what it looks like:
{
"packageDirectories": [
{
"path": "force-app",
"default": true,
"package": "curiousancestry",
"versionNumber": "0.5.0.NEXT"
},
{
"path": "child-app",
"default": false,
"package": "childpackage",
"versionNumber": "0.1.0.NEXT",
"versionName": "ver 0.1",
"dependencies": [
{
"package": "curiousancestry",
"versionNumber": "0.5.0.LATEST"
}
]
}
],
"namespace": "curiousancestry",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "48.0",
"packageAliases": {
"curiousancestry": "parentPackageId",
"curiousancestry@0.1.0-2": "parentVersion1",
"curiousancestry@0.2.0-1": "parentVersion2",
"curiousancestry@0.3.0-1": "parentVersion3",
"curiousancestry@0.4.0-1": "parentVersion4",
"curiousancestry@0.5.0-1": "parentVersion5",
"2gpchild": "childPackageId"
}
}
What we’ve done here is told the system that when we package up the contents of the child-app
folder, it will register a dependency on version 0.5.0 of the parent package. It will not be able to install unless parent package is installed first.
In the child-app
directory, add a directory called main
, and under main, create default
.
default
as a sources root. This will allow you to save metadata you place in here to the Scratch Org you’re working in.Now let’s add the following two classes:
public with sharing class ChildDemo {
public String checkHowMad (Integer baseMad) {
Integer howMad = CuriousIndeed.getMadOrMadder(baseMad);
if (howMad <= 0) {
return 'Not';
} else if (howMad < 5) {
return 'Somewhat';
} else if (howMad < 11) {
return 'Very';
} else {
return 'Incredibly';
}
}
}
@IsTest(SeeAllData=false)
private with sharing class ChildDemoTest {
@IsTest
private static void TestCheckHowMad_Somewhat () {
Integer controlMad = 1;
Test.startTest();
String resultMadDescription = ChildDemo.checkHowMad(controlMad);
Test.stopTest();
System.assertEquals('Somewhat', resultMadDescription);
}
@IsTest
private static void TestCheckHowMad_Very () {
Integer controlMad = 5;
Test.startTest();
String resultMadDescription = ChildDemo.checkHowMad(controlMad);
Test.stopTest();
System.assertEquals('Very', resultMadDescription);
}
@IsTest
private static void TestCheckHowMad_Incredibly () {
Integer controlMad = 5;
Test.startTest();
String resultMadDescription = ChildDemo.checkHowMad(controlMad);
Test.stopTest();
System.assertEquals('Very', resultMadDescription);
}
@IsTest
private static void TestCheckHowMad_Not () {
Integer controlMad = -5;
Test.startTest();
CuriousIndeed.minimumMad = -10;
String resultMadDescription = ChildDemo.checkHowMad(controlMad);
Test.stopTest();
System.assertEquals('Not', resultMadDescription);
}
}
As you can see here we’ve referenced code in the parent package, but we’re in the child package directory. Let’s package it now.
sfdx force:package:version:create --path child-app/ --codecoverage --installationkeybypass --wait 10
I did something wrong on purpose to illustrate something important…. In version 0.5 of the parent app we made the class public.
ERROR running force:package:version:create: ChildDemo: Type is not visible: curiousancestry.CuriousIndeed,ChildDemoTest: Type is not visible: curiousancestry.CuriousIndeed
This means that even though we’re in the same namespace we forgot something in the base/parent package. That’s what @NamespaceAccessible
is for. We could also use global instead of public, but this would expose things to customers. To make everything work, I’m going to add @NamespaceAccessible
and release version 0.6 of the parent package while keeping everything public. This is what the CuriousIndeed
class looks like now:
@NamespaceAccessible
public with sharing class CuriousIndeed {
@NamespaceAccessible
public static Integer minimumMad = 1;
@NamespaceAccessible
public static Integer getMadOrMadder (Integer isMad) {
// They didn't specify, let's set the default.
if (isMad == null) {
isMad = 0;
}
// The goal here is to make them more mad on the way out.
isMad++;
// return our defined minimum
return (minimumMad > isMad) ? minimumMad : isMad;
}
}
Here I updated the sfdx-package.json
so that it would build version 0.6, pushed the code, created a new package version, and released the parent package again. When done your sfdx-project.json
should look like this (note the update to dependencies):
{
"packageDirectories": [
{
"path": "force-app",
"default": true,
"package": "curiousancestry",
"versionNumber": "0.6.0.NEXT"
},
{
"path": "child-app",
"package": "2gpchild",
"versionName": "ver 0.1",
"versionNumber": "0.1.0.NEXT",
"default": false,
"dependencies": [
{
"package": "curiousancestry",
"versionNumber": "0.6.0.LATEST"
}
]
}
],
"namespace": "curiousancestry",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "48.0",
"packageAliases": {
"curiousancestry": "parentPackageId",
"curiousancestry@0.1.0-2": "parentVersion1",
"curiousancestry@0.2.0-1": "parentVersion2",
"curiousancestry@0.3.0-1": "parentVersion3",
"curiousancestry@0.4.0-1": "parentVersion4",
"curiousancestry@0.5.0-1": "parentVersion5",
"2gpchild": "childPackageId",
"curiousancestry@0.6.0-1": "parentVersion6"
}
}
Let’s try creating a new version of the child app now…
sfdx force:package:version:create --path child-app/ --codecoverage --installationkeybypass --wait 10
Result:
Successfully created the package version [removed]. Subscriber Package Version Id: childVersion1
Now grab the package version id (starts with 04t
) returned from the create command, and promote it.
sfdx force:package:version:promote -p child_version_01 -n
At this point you can play with installing version 0.6 of the parent and version 0.1 of the child.
Most things in this setup work great when you run force:source:push
. One thing that doesn’t at the time of this writing is deploying fields to the same Object from multiple packages. Let’s use Account
for our example.
If your directory structure looks like this:
force-app/main/default/objects/Account/fields/CustomField1__c.field
child-app/main/default/objects/Account/fields/CustomField2__c.field
Then expect CustomField1__c
to deploy, but CustomField2__c
will not. It looks like they’ll still package up just fine, but they will require manual creation in the SO in order to use them. (there are other options, but manual creation for me is the simplest)
In all of our examples we were calling and executing 100% of the code from the parent package in the unit tests from the child package. So everything played nicely…
Let’s pretend the child package did not call the code in the first package. Or maybe it didn’t fire a trigger - and triggers need at least 1% coverage. This rule still holds… Well, when packaging the child you’ll experience code coverage issues. I’ll include some skeletonized versions of the child package code as an example.
public with sharing class ChildDemo {
public static String checkHowMad (Integer baseMad) {
return 'Not';
}
}
@IsTest(SeeAllData=false)
private with sharing class ChildDemoTest {
@IsTest
private static void TestCheckHowMad_Not () {
Integer controlMad = -5;
Test.startTest();
String resultMadDescription = ChildDemo.checkHowMad(controlMad);
Test.stopTest();
System.assertEquals('Not', resultMadDescription);
}
}
Also, increment the child version in your sfdx-project.json
:
{
"packageDirectories": [
{
"path": "force-app",
"default": true,
"package": "curiousancestry",
"versionNumber": "0.6.0.NEXT"
},
{
"path": "child-app",
"package": "2gpchild",
"versionName": "ver 0.2",
"versionNumber": "0.2.0.NEXT",
"default": false,
"dependencies": [
{
"package": "curiousancestry",
"versionNumber": "0.6.0.LATEST"
}
]
}
],
"namespace": "curiousancestry",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "48.0",
"packageAliases": {
"curiousancestry": "parentPackageId",
"curiousancestry@0.1.0-2": "parentVersion1",
"curiousancestry@0.2.0-1": "parentVersion2",
"curiousancestry@0.3.0-1": "parentVersion3",
"curiousancestry@0.4.0-1": "parentVersion4",
"curiousancestry@0.5.0-1": "parentVersion5",
"2gpchild": "childPackageId",
"curiousancestry@0.6.0-1": "parentVersion6",
"2gpchild@0.1.0-1": "childVersion1"
}
}
Crete a new package version, and try to promote it:
sfdx force:package:version:create --path child-app/ --codecoverage --installationkeybypass --wait 10
sfdx force:package:version:promote -p childVersion2
You will see this:
ERROR running force:package:version:promote: The code coverage required to promote this version has not been met. Please add additional test coverage and ensure the code coverage check passes during version creation.
So… for now be prepared to have a lot of unit tests in your child packages, even if there’s not much to them.
I hope this has been helpful. I think 2GP is really cool. Setting up a directory structure and project definition seems pretty obvious now, but it wasn’t so when I first went through it. If you find more weirdities, or anything that has changed since this was published please reach out.
Thanks for reading!