Package and Distribute Your App

You can distribute a Multi-Framework app as a second-generation package (2GP), which wraps your UIBundle and its related metadata into a single, versioned artifact that you install into a subscriber org, upgrade in place, and cleanly uninstall.

What You Can Package 

You choose a package type when you create the package. Both types support React and Angular apps.

  • Unlocked (--package-type Unlocked) - no namespace required, and installed components stay editable in the subscriber org. Supports both internal and external apps. Use it for internal distribution, proofs of concept, and rapid iteration.
  • Managed (--package-type Managed) - requires a namespace, installed components are locked, and this is the only type eligible for AppExchange. Supports internal apps only. Use it to distribute to external customers or to list on AppExchange.

For the full comparison of the two types, see Second-Generation Managed Packages and Unlocked Packages in the packaging guide.

Prepare Your UIBundle for Packaging 

Beyond the standard packaging setup (a Dev Hub with second-generation packaging enabled, and a registered namespace for a managed package), packaging a UIBundle has a few specific requirements.

Set the Source API Version 

Set sourceApiVersion to 67.0 or later in sfdx-project.json.

1{
2  "sourceApiVersion": "67.0",
3  "packageDirectories": [
4    {
5      "path": "force-app",
6      "package": "MyApp",
7      "versionName": "ver 0.1",
8      "versionNumber": "0.1.0.NEXT",
9      "default": true
10    }
11  ]
12}

For a managed package, also set the namespace property to your registered namespace.

Ensure you set the API version earlier to 67.0 or later.

Important

Include the App's Metadata 

Make sure your package directory contains your UIBundle and its related metadata. An internal app includes a CustomApplication that references the UIBundle. An external app includes its Experience Cloud site metadata. See Project Structure and Metadata. Because a managed package supports internal apps only, package an internal (CustomApplication-based) app when you use the managed type.

Build the Bundle 

Build your UI bundle so the packaged assets are current before you create a package version.

1cd force-app/main/default/uiBundles/myapp
2npm install
3npm run build

Use the local dev server (npm run dev) as your inner development loop while you build the app. A full build, version create, and install round trip takes several minutes, so don’t use sf package version create to iterate on UI changes. See Preview and Run Your App Locally.

Note

Prepare for AppExchange Submission (Managed Packages) 

If you’re submitting your managed package to AppExchange for Security Review, complete these two prerequisites before submission. Unlocked packages do not go through AppExchange Security Review and do not require these steps.

Enable Sourcemap Emission 

The Salesforce Code Analyzer verifies that the compiled JavaScript in dist/ traces back to your source code by reading the sourcemaps your build tool emits. Sourcemaps are required for the analyzer to run its UI Bundle checks.

In your vite.config.ts, set sourcemap to true in the build block:

1export default defineConfig(({ mode }) => {
2  return {
3    base: "./",
4    plugins: [
5      // tailwindcss(), react(), salesforce(), ...
6    ],
7    build: {
8      outDir: resolve(__dirname, "dist"),
9      assetsDir: "assets",
10      sourcemap: true, // ← must be true for AppExchange submission
11    },
12    // resolve: { ... },
13    // test: { ... },
14  };
15});

Verify that every .js file under dist/ has a co-located .js.map, and that the sources[] array inside each map uses relative paths-no absolute paths like /Users/… or C:\….

Run the Code Analyzer Scan 

Before submitting, run the Salesforce Code Analyzer with the uibundle selector to scan your compiled bundle.

First, ensure the plugin is version 5.16.0 or later:

1sf plugins install @salesforce/plugin-code-analyzer@latest
2sf plugins   # confirm code-analyzer 5.16.0 or later

Then, from your project root after you’ve run npm run build, run the scan:

1sf code-analyzer run \
2  --rule-selector uibundle \
3  --rule-selector Recommended \
4  --output-file CodeAnalyzerReport.html

The uibundle selector runs UI Bundle integrity checks, and Recommended runs the standard cross-engine baseline (ESLint, RetireJS, and others). The two selectors compose additively with no overlap.

Verify the Packaged UIBundle 

After you install the package in a subscriber org, confirm the UIBundle component landed by querying it through the Tooling API.

1sf data query \
2  --query "SELECT Id, DeveloperName, IsActive, ManageableState, NamespacePrefix FROM UIBundle" \
3  --target-org my-subscriber --use-tooling-api
  • For a managed package, ManageableState is installed (locked) and NamespacePrefix is your namespace.
  • For an unlocked package, ManageableState is installedEditable.
  • In both cases, IsActive is true.

Then open the app to confirm it renders: from the subscriber org, open the App Launcher, search for your app, and open it.

An upgrade doesn’t overwrite the UIBundle’s IsActive value. If a new version needs to be activated, activate it manually after you upgrade.

Note

See Also