Develop UI bundles with seamless Salesforce integration
The sf ui-bundle dev command enables local development of modern web applications (React, Vue, Angular, etc.) with automatic Salesforce authentication. It intelligently discovers your UI bundle configuration, handles proxy routing, injects authentication headers, and supports hot reload - so you can focus on building your UI bundle.
- Auto-Discovery: Automatically finds UI bundles in
uiBundles/folder - Optional Manifest:
ui-bundle.jsonis optional - uses sensible defaults - Auto-Selection: Automatically selects UI bundle when running from inside its folder
- Interactive Selection: Prompts with arrow-key navigation when multiple UI bundles exist
- Authentication Injection: Automatically adds Salesforce auth headers to API calls
- Intelligent Routing: Routes requests to dev server or Salesforce based on URL patterns
- Hot Module Replacement: Full HMR support for Vite, Webpack, and other bundlers
- Error Detection: Displays helpful error pages with fix suggestions
- Framework Agnostic: Works with any web framework
my-sfdx-project/
├── sfdx-project.json
└── force-app/main/default/uiBundles/
└── my-bundle/
├── my-bundle.uibundle-meta.xml
├── package.json
├── src/
└── ui-bundle.json
sf ui-bundle dev --target-org myOrg --openBrowser opens to http://localhost:4545 with your UI bundle running and Salesforce authentication ready.
Note:
{name}.uibundle-meta.xmlis required to identify a valid UI bundleui-bundle.jsonis optional for dev configuration. If not present, defaults to:
- Name: From meta.xml filename or folder name
- Dev command:
npm run dev- Manifest watching: Disabled
sf ui-bundle dev [OPTIONS]| Option | Short | Description | Default |
|---|---|---|---|
--target-org |
-o |
Salesforce org alias or username | Required |
--name |
-n |
UI bundle name (folder name) | Auto-discover |
--url |
-u |
Explicit dev server URL | Auto-detect |
--port |
-p |
Proxy server port | 4545 |
--open |
-b |
Open browser automatically | false |
# Simplest - auto-discovers ui-bundle.json
sf ui-bundle dev --target-org myOrg
# With browser auto-open
sf ui-bundle dev --target-org myOrg --open
# Specify UI bundle by name (when multiple exist)
sf ui-bundle dev --name myBundle --target-org myOrg
# Custom port
sf ui-bundle dev --target-org myOrg --port 8080
# Explicit dev server URL (skip auto-detection)
sf ui-bundle dev --target-org myOrg --url http://localhost:5173
# Debug mode
SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrgThe command discovers UI bundles using a simplified, deterministic algorithm. UI bundles are identified by the presence of a {name}.uibundle-meta.xml file (SFDX metadata format). The optional ui-bundle.json file provides dev configuration.
flowchart TD
Start["sf ui-bundle dev"] --> CheckInside{"Inside uiBundles/<br/>ui bundle folder?"}
CheckInside -->|Yes| HasNameInside{"--name provided?"}
HasNameInside -->|Yes, different| ErrorConflict["Error: --name conflicts<br/>with current directory"]
HasNameInside -->|No or same| AutoSelect["Auto-select current UI bundle"]
CheckInside -->|No| CheckSFDX{"In SFDX project?<br/>(sfdx-project.json)"}
CheckSFDX -->|Yes| CheckPath["Check force-app/main/<br/>default/uiBundles/"]
CheckPath --> HasName{"--name provided?"}
CheckSFDX -->|No| CheckMetaXml{"Current dir has<br/>.uibundle-meta.xml?"}
CheckMetaXml -->|Yes| UseStandalone["Use current dir as UI bundle"]
CheckMetaXml -->|No| ErrorNone["Error: No UI bundle found"]
HasName -->|Yes| SearchByName["Find UI bundle by name"]
HasName -->|No| Prompt["Interactive selection prompt<br/>(always, even if 1 UI bundle)"]
SearchByName --> UseBundle["Use UI bundle"]
AutoSelect --> UseBundle
UseStandalone --> UseBundle
Prompt --> UseBundle
UseBundle --> StartDev["Start dev server and proxy"]
| Scenario | Behavior |
|---|---|
--name myBundle provided |
Finds UI bundle by name, starts dev server |
| Running from inside UI bundle folder | Auto-selects that UI bundle |
--name conflicts with current dir |
Error: must match current UI bundle or run from project root |
| At SFDX project root | Always prompts for UI bundle selection |
| Outside SFDX project with meta.xml | Uses current directory as standalone UI bundle |
| No UI bundle found | Shows error with helpful message |
my-sfdx-project/
├── sfdx-project.json # SFDX project marker
└── force-app/main/default/
└── uiBundles/ # Standard SFDX location
├── bundle-one/ # UI Bundle 1 (with dev config)
│ ├── bundle-one.uibundle-meta.xml # Required: identifies as UI bundle
│ ├── ui-bundle.json # Optional: dev configuration
│ ├── package.json
│ └── src/
└── bundle-two/ # UI Bundle 2 (no dev config)
├── bundle-two.uibundle-meta.xml # Required
├── package.json
└── src/
The command uses a simplified, deterministic approach:
- Inside UI bundle folder: If running from
uiBundles/<bundle>/or deeper, auto-selects that UI bundle - SFDX project root: Uses fixed path
force-app/main/default/uiBundles/ - Standalone: If current directory has a
.uibundle-meta.xmlfile, uses it directly
Important: Only directories containing a {name}.uibundle-meta.xml file are recognized as valid UI bundles.
When multiple UI bundles are found, you'll see an interactive prompt:
Found 3 UI bundles in project
? Select the UI bundle to run: (Use arrow keys)
❯ bundle-one (uiBundles/bundle-one)
bundle-two (uiBundles/bundle-two) [no manifest]
bundle-three (uiBundles/bundle-three)
Format:
- With manifest:
folder-name (path) - No manifest:
folder-name (path) [no manifest]
┌─────────────────────────────────────────────────┐
│ Your Browser │
│ http://localhost:4545 │
└───────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Proxy Server (Port 4545) │
│ │
│ Routes requests based on URL pattern: │
│ • /services/* → Salesforce (with auth) │
│ • Everything else → Dev Server │
└─────────┬─────────────────────┬─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌────────────────────────┐
│ Dev Server │ │ Salesforce Instance │
│ (localhost:5173)│ │ + Auth Headers Added │
│ React/Vue/etc │ │ + API Calls │
└─────────────────┘ └────────────────────────┘
Static assets (JS, CSS, HTML, images):
Browser → Proxy → Dev Server → Response
Salesforce API calls (/services/*):
Browser → Proxy → [Auth Headers Injected] → Salesforce → Response
The command operates in two distinct modes based on configuration:
| Mode | Configuration | Behavior |
|---|---|---|
| Command mode | dev.command is set (or default npm run dev) |
CLI starts the dev server. URL defaults to http://localhost:5173. Override with dev.url or --url if your dev server uses a different port. |
| URL-only mode | dev.url or --url only (no dev.command) |
CLI assumes the dev server is already running. Does not start the dev server. Starts proxy only and forwards to the given URL. |
URL precedence: --url flag > dev.url in manifest > default http://localhost:5173 (when command is used)
The ui-bundle.json file is optional. All fields are also optional - missing fields use defaults.
| Field | Type | Description | Default |
|---|---|---|---|
dev.command |
string | Command to start the dev server (e.g., npm run dev). When set, the CLI starts the dev server and uses default URL http://localhost:5173 unless overridden. |
npm run dev |
dev.url |
string | Dev server URL. Command mode: Override the default 5173 port if needed. URL-only mode: Required—the CLI assumes the server is already running and does not start it. | http://localhost:5173 |
Command mode (CLI starts dev server):
{
"dev": {
"command": "npm run dev"
}
}- CLI runs
npm run devand waits for the server to be ready - Default URL:
http://localhost:5173 - Override port: add
"url": "http://localhost:3000"if your dev server uses a different port
URL-only mode (proxy only, server already running):
{
"dev": {
"url": "http://localhost:5173"
}
}- No
dev.command— CLI does not start the dev server - You must start the dev server yourself (e.g.,
npm run devin another terminal) - CLI starts only the proxy and forwards to the given URL
No manifest (uses defaults):
- Dev command:
npm run dev - Default URL:
http://localhost:5173 - Manifest watching: disabled
{
"routing": {
"rewrites": [{ "route": "/api/:path*", "target": "/services/apexrest/:path*" }],
"redirects": [{ "route": "/old-path", "target": "/new-path", "statusCode": 301 }],
"trailingSlash": "never",
"fallback": "/index.html"
}
}uiBundles/
└── my-bundle/
├── package.json # Has "scripts": { "dev": "vite" }
└── src/
Run: sf ui-bundle dev --target-org myOrg
Console output:
Warning: No ui-bundle.json found for UI bundle "my-bundle"
Location: my-bundle
Using defaults:
→ Name: "my-bundle" (derived from folder)
→ Command: "npm run dev"
→ Manifest watching: disabled
💡 To customize, create a ui-bundle.json file in your UI bundle directory.
✅ Using UI bundle: my-bundle (uiBundles/my-bundle)
✅ Ready for development!
→ Proxy: http://localhost:4545 (open this in your browser)
→ Dev server: http://localhost:5173
Press Ctrl+C to stop
{
"dev": {
"command": "npm run dev"
},
"routing": {
"rewrites": [{ "route": "/api/:path*", "target": "/services/apexrest/:path*" }],
"trailingSlash": "never"
}
}Edit ui-bundle.json while running - changes apply automatically:
# Console output when you change ui-bundle.json:
Manifest changed detected
✓ Manifest reloaded successfully
Dev server URL updated to: http://localhost:5174Note: Manifest watching is only enabled when
ui-bundle.jsonexists. UI bundles without manifests don't have this feature.
The proxy continuously monitors dev server availability:
- Displays "No Dev Server Detected" page when server is down
- Auto-refreshes when server comes back up
- Shows helpful suggestions for common issues
Full Hot Module Replacement support through the proxy:
- Vite HMR (
/@vite/*,/__vite_hmr) - Webpack HMR (
/__webpack_hmr) - Works with React Fast Refresh, Vue HMR, etc.
Automatically detects Salesforce Code Builder environment and binds to 0.0.0.0 for proper port forwarding in cloud environments.
The --url flag overrides the dev server URL. Behavior depends on whether you have a command configured:
| Scenario | Command in manifest? | --url behavior |
|---|---|---|
| URL-only mode | No | Required. CLI assumes the server is already running and does not start it. Use when you run the dev server yourself. |
| Command mode | Yes | Optional override. Default is http://localhost:5173. Use --url to point to a different port. |
| URL reachable | Either | Proxy-only: skips starting dev server, starts proxy only |
| URL not reachable | Yes (command) | Starts dev server and warns if actual URL differs from --url |
| URL not reachable | No (URL-only) | Error: server must be running at the given URL |
When you run the dev server yourself:
# Terminal 1: Start your dev server manually
cd my-bundle
npm run dev
# Output: Local: http://localhost:5173/
# Terminal 2: Connect proxy to your running server
sf ui-bundle dev --url http://localhost:5173 --target-org myOrgOutput:
✅ URL http://localhost:5173 is already available, skipping dev server startup (proxy-only mode)
✅ Ready for development!
→ http://localhost:4545 (open this URL in your browser)
When using dev.command, the default URL is http://localhost:5173. Override with --url if your dev server uses a different port:
sf ui-bundle dev --url http://localhost:3000 --target-org myOrgIf the URL is not reachable, the CLI starts the dev server and uses the actual URL (with a warning if it differs).
Ensure your UI bundle has the required .uibundle-meta.xml file:
force-app/main/default/uiBundles/
└── my-bundle/
├── my-bundle.uibundle-meta.xml # Required!
├── package.json
└── ui-bundle.json # Optional (for dev config)
The .uibundle-meta.xml file identifies a valid SFDX UI bundle. Without it, the directory is ignored.
This error occurs when you're inside one UI bundle folder but try to run a different one:
# You're in FirstBundle folder but trying to run SecondBundle
cd uiBundles/FirstBundle
sf ui-bundle dev --name SecondBundle --target-org myOrg # Error!Solutions:
- Remove
--nameto use the current UI bundle - Navigate to the project root and use
--name - Navigate to the correct UI bundle folder
The --name flag matches the folder name of the UI bundle.
# This looks for UI bundle named "myBundle"
sf ui-bundle dev --name myBundle --target-org myOrgInstall dependencies in your UI bundle folder:
cd uiBundles/my-bundle
npm install- Ensure dev server is running:
npm run dev - Verify URL in
ui-bundle.jsonis correct - Try explicit URL:
sf ui-bundle dev --url http://localhost:5173 --target-org myOrg
# Use a different port
sf ui-bundle dev --port 8080 --target-org myOrg
# Or find and kill the process using the port
lsof -i :4545
kill -9 <PID>Re-authorize your Salesforce org:
sf org login web --alias myOrgEnable detailed logging by setting SF_LOG_LEVEL=debug. Debug logs are written to the SF CLI log file (not stdout).
Step 1: Start log tail in Terminal 1
# Tail today's log file, filtering for UiBundleDev messages
tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered UiBundleDev
# Or for cleaner output (requires jq):
tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered UiBundleDev | jq -r '.msg'Step 2: Run command in Terminal 2
SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrgExample debug output:
Discovering ui-bundle.json manifest(s)...
Using UI bundle: myBundle at uiBundles/my-bundle
Manifest loaded: myBundle
Starting dev server with command: npm run dev
Dev server ready at: http://localhost:5173/
Using authentication for org: user@example.com
Starting proxy server on port 4545...
Proxy server running on http://localhost:4545
The command integrates with the Salesforce VSCode UI Preview extension (salesforcedx-vscode-ui-preview):
- Extension detects
ui-bundle.jsonin workspace - User clicks "Preview" button on the file
- Extension executes:
sf ui-bundle dev --target-org <org> --open - If multiple UI bundles exist, uses
--nameto specify which one - Browser opens with your UI bundle running
For scripting and CI/CD, use the --json flag:
sf ui-bundle dev --target-org myOrg --jsonOutput:
{
"status": 0,
"result": {
"url": "http://localhost:4545",
"devServerUrl": "http://localhost:5173"
}
}cd /path/to/plugin-ui-bundle-dev
# Install dependencies
yarn install
# Build
yarn build
# Link to SF CLI
sf plugins link .
# Verify installation
sf pluginsyarn build # Rebuild - no re-linking neededplugin-ui-bundle-dev/
├── src/
│ ├── commands/ui-bundle/
│ │ └── dev.ts # Main command implementation
│ ├── config/
│ │ ├── manifest.ts # Manifest type definitions
│ │ ├── ManifestWatcher.ts # File watching and hot reload
│ │ ├── webappDiscovery.ts # Auto-discovery logic
│ │ └── types.ts # Shared TypeScript types
│ ├── proxy/
│ │ ├── ProxyServer.ts # HTTP/WebSocket proxy server
│ │ ├── handler.ts # Request routing and forwarding
│ │ └── routing.ts # URL pattern matching
│ └── server/
│ └── DevServerManager.ts # Dev server process management
├── messages/
│ └── ui-bundle.dev.md # CLI messages and help text
└── schemas/
└── ui__bundle-dev.json # JSON schema for output
| Component | Purpose |
|---|---|
dev.ts |
Command orchestration and lifecycle |
webappDiscovery.ts |
SFDX project detection and UI bundle discovery |
ProxyServer.ts |
HTTP proxy with WebSocket support |
DevServerManager.ts |
Dev server process spawning and monitoring |
ManifestWatcher.ts |
ui-bundle.json file watching for hot reload |
Repository: github.com/salesforcecli/plugin-ui-bundle-dev