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.
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.
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/myapp2npm install3npm 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:
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@latest2sf plugins # confirm code-analyzer 5.16.0 or later
Then, from your project root after you’ve run npm run build, run the scan:
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.