Foglight Solutions - Second Generation Packaging - Two Packages One Namespace

Second Generation Packaging - Two Packages One Namespace

June 8, 2020 by Trieste LaPorte

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.

Purpose

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.

Methodology

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.

How is 2GP different from 1GP?

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:

  • 1GP: For each package we want to create, we have to create a separate packaging org. In the ‘child’ orgs we must first install our ‘parent’ package, and then when we write code that references components from the ‘parent’ package we include the namespace prefix of the parent. Each package usually requires its own VCS repos and project directory. Dependencies are determined solely by which packages refer to components in other packages.
  • 2GP: Here multiple packages can happily exist in the same project directory, and the same version control repository. Dependencies between packages are defined in the sfdx-project.json file. Each segment below will dive deeper into the differences.

The Main Package

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"
    }
}

The Add-on Package

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.

Packaging the Child App

In the child-app directory, add a directory called main, and under main, create default.

  • At this time, if you’re using Illuminated Cloud 2, right click and mark 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.

Weird Stuff

Custom fields on the same object in two packages

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)

Unit tests in second package must cover code in first

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.

Conclusion

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!

ABOUT THE AUTHOR

Trieste LaPorte | Technical Architect

Trieste is a Technical Architect with Foglight Solutions and has been breaking the platform for a decade.