From 21b892ab508f7ed0029abce5689fdb32510375a8 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 27 Mar 2026 13:52:00 +0530 Subject: [PATCH 01/16] refactor: rename webapp dev command and plugin to ui-bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename `sf webapp dev` command to `sf ui-bundle dev` and update plugin identity: - Rename src/commands/webapp/dev.ts to src/commands/ui-bundle/dev.ts - Rename messages/webapp.dev.md to messages/ui-bundle.dev.md - Rename schemas/webapp-dev.json to schemas/ui-bundle-dev.json - Rename SF_WEBAPP_DEV_GUIDE.md to SF_UI_BUNDLE_DEV_GUIDE.md - Rename test/commands/webapp/ to test/commands/ui-bundle/ - Rename plugin from @salesforce/plugin-app-dev to @salesforce/plugin-ui-bundle-dev - Update command-snapshot.json: webapp:dev β†’ ui-bundle:dev - Update package.json: topics webapp β†’ ui-bundle, homepage and repository URLs - Update webappDiscovery.ts: path segment and folder constant webui β†’ uiBundles - Update COMMANDS.md and README.md with ui-bundle command and package references - Update src/proxy/ProxyServer.ts with associated changes W-21575874 Made-with: Cursor --- .env.template | 2 +- COMMANDS.md | 40 +-- README.md | 28 +- ..._DEV_GUIDE.md => SF_UI_BUNDLE_DEV_GUIDE.md | 120 ++++----- command-snapshot.json | 4 +- messages/ui-bundle.dev.md | 247 ++++++++++++++++++ messages/webapp.dev.md | 240 ----------------- package.json | 10 +- .../{webapp-dev.json => ui__bundle-dev.json} | 9 +- src/commands/{webapp => ui-bundle}/dev.ts | 26 +- src/config/types.ts | 2 +- src/config/webappDiscovery.ts | 92 +++---- src/proxy/ProxyServer.ts | 31 +-- .../{webapp => ui-bundle}/_cleanup.nut.ts | 0 .../commands/{webapp => ui-bundle}/dev.nut.ts | 54 ++-- .../{webapp => ui-bundle}/dev.test.ts | 2 +- .../{webapp => ui-bundle}/devPort.nut.ts | 2 +- .../{webapp => ui-bundle}/devWithUrl.nut.ts | 14 +- .../helpers/devServerUtils.ts | 6 +- .../helpers/webappProjectUtils.ts | 43 +-- test/config/webappDiscovery.test.ts | 20 +- 21 files changed, 505 insertions(+), 487 deletions(-) rename SF_WEBAPP_DEV_GUIDE.md => SF_UI_BUNDLE_DEV_GUIDE.md (76%) create mode 100644 messages/ui-bundle.dev.md delete mode 100644 messages/webapp.dev.md rename schemas/{webapp-dev.json => ui__bundle-dev.json} (74%) rename src/commands/{webapp => ui-bundle}/dev.ts (96%) rename test/commands/{webapp => ui-bundle}/_cleanup.nut.ts (100%) rename test/commands/{webapp => ui-bundle}/dev.nut.ts (83%) rename test/commands/{webapp => ui-bundle}/dev.test.ts (99%) rename test/commands/{webapp => ui-bundle}/devPort.nut.ts (98%) rename test/commands/{webapp => ui-bundle}/devWithUrl.nut.ts (97%) rename test/commands/{webapp => ui-bundle}/helpers/devServerUtils.ts (96%) rename test/commands/{webapp => ui-bundle}/helpers/webappProjectUtils.ts (77%) diff --git a/.env.template b/.env.template index d80deb8..9b42f3e 100644 --- a/.env.template +++ b/.env.template @@ -3,7 +3,7 @@ # # Option A: AUTH_URL (simplest β€” recommended for getting started) # 1. sf org login web --alias nut-org -# 2. sf org display -o nut-org --json | jq -r .result.sfdxAuthUrl +# 2. sf org display -o nut-org --verbose --json | jq -r .result.sfdxAuthUrl # 3. Paste the value below # TESTKIT_AUTH_URL= diff --git a/COMMANDS.md b/COMMANDS.md index 2dde89a..0ee6eb8 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -2,43 +2,47 @@ -- [`sf webapp dev`](#sf-webapp-dev) +- [`sf ui-bundle dev`](#sf-webui-dev) -## `sf webapp dev` +## `sf ui-bundle dev` -Preview a web app locally without needing to deploy +Start a local development proxy server for webui ui-bundle development with Salesforce authentication. ``` USAGE - $ sf webapp dev -n [--json] [--flags-dir ] [-t ] [-p ] + $ sf ui-bundle dev -n -o [--json] [--flags-dir ] [-u ] [-p ] [--open] -FLAGS - -n, --name= (required) Identifies the Web Application - -p, --port= [default: 5173] Port for the dev server - -t, --target= Selects which Web Application target to use for the preview (e.g., Lightning App, Site) +REQUIRED FLAGS + -n, --name= Name of the webapp (must match webapplication.json) + -o, --target-org= Salesforce org to authenticate against + +OPTIONAL FLAGS + -u, --url= Dev server URL. Command mode: override default 5173. URL-only: required (server must be running) + -p, --port= Proxy server port (default: 4545) + --open Open browser automatically GLOBAL FLAGS --flags-dir= Import flag values from a directory. --json Format output as json. DESCRIPTION - Preview a web app locally without needing to deploy - - Starts a local development server for a Web Application, using the local project files. This enables rapid - development with hot reloading and immediate feedback. + Starts a local HTTP proxy that injects Salesforce authentication and routes + requests between your dev server and Salesforce APIs. In command mode, + spawns and monitors the dev server (default URL: localhost:5173). In + URL-only mode, connects to an already-running dev server. EXAMPLES - Start the development server: + Command mode (CLI starts dev server, default port 5173): - $ sf webapp dev --name myWebApp + $ sf ui-bundle dev --name myapp --target-org myorg --open - Start the development server with a specific target: + URL-only mode (dev server already running): - $ sf webapp dev --name myWebApp --target "LightningApp" + $ sf ui-bundle dev --name myapp --target-org myorg --url http://localhost:5173 --open - Start the development server on a custom port: + Custom proxy port: - $ sf webapp dev --name myWebApp --port 8080 + $ sf ui-bundle dev --name myapp --target-org myorg --port 8080 --open ``` diff --git a/README.md b/README.md index 0c2d284..59bcdde 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# plugin-app-dev +# plugin-ui-bundle-dev -[![NPM](https://img.shields.io/npm/v/@salesforce/plugin-app-dev.svg?label=@salesforce/plugin-app-dev)](https://www.npmjs.com/package/@salesforce/plugin-app-dev) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-app-dev.svg)](https://npmjs.org/package/@salesforce/plugin-app-dev) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) +[![NPM](https://img.shields.io/npm/v/@salesforce/plugin-ui-bundle-dev.svg?label=@salesforce/plugin-ui-bundle-dev)](https://www.npmjs.com/package/@salesforce/plugin-ui-bundle-dev) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-ui-bundle-dev.svg)](https://npmjs.org/package/@salesforce/plugin-ui-bundle-dev) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) # Salesforce CLI App Dev Plugin @@ -24,7 +24,7 @@ We always recommend using the latest version of these commands bundled with the 1. **Install the plugin:** ```bash - sf plugins install @salesforce/plugin-app-dev + sf plugins install @salesforce/plugin-ui-bundle-dev ``` 2. **Authenticate with Salesforce:** @@ -50,12 +50,12 @@ We always recommend using the latest version of these commands bundled with the 4. **Start development:** ```bash - sf webapp dev --name myapp --target-org myorg --open + sf ui-bundle dev --name myapp --target-org myorg --open ``` ## Documentation -πŸ“š **[Complete Guide](SF_WEBAPP_DEV_GUIDE.md)** - Comprehensive documentation covering: +πŸ“š **[Complete Guide](SF_UI_BUNDLE_DEV_GUIDE.md)** - Comprehensive documentation covering: - Overview and architecture - Getting started (5-minute quick start) @@ -69,7 +69,7 @@ We always recommend using the latest version of these commands bundled with the ## Install ```bash -sf plugins install @salesforce/plugin-app-dev@x.y.z +sf plugins install @salesforce/plugin-ui-bundle-dev@x.y.z ``` ## Issues @@ -101,7 +101,7 @@ To build the plugin locally, make sure to have yarn installed and run the follow ```bash # Clone the repository -git clone git@github.com:salesforcecli/plugin-app-dev +git clone git@github.com:salesforcecli/plugin-ui-bundle-dev # Install the dependencies and compile yarn && yarn build @@ -125,9 +125,9 @@ sf plugins ## Commands -### `sf webapp dev` +### `sf ui-bundle dev` -Start a local development proxy server for webapp development with Salesforce authentication. +Start a local development proxy server for ui-bundle development with Salesforce authentication. **Two operating modes:** @@ -136,7 +136,7 @@ Start a local development proxy server for webapp development with Salesforce au ```bash USAGE - $ sf webapp dev --name --target-org [options] + $ sf ui-bundle dev --name --target-org [options] REQUIRED FLAGS -n, --name= Name of the webapp (must match webapplication.json) @@ -156,18 +156,18 @@ DESCRIPTION EXAMPLES Command mode (CLI starts dev server, default port 5173): - $ sf webapp dev --name myapp --target-org myorg --open + $ sf ui-bundle dev --name myapp --target-org myorg --open URL-only mode (dev server already running): - $ sf webapp dev --name myapp --target-org myorg --url http://localhost:5173 --open + $ sf ui-bundle dev --name myapp --target-org myorg --url http://localhost:5173 --open Custom proxy port: - $ sf webapp dev --name myapp --target-org myorg --port 8080 --open + $ sf ui-bundle dev --name myapp --target-org myorg --port 8080 --open SEE ALSO - - Complete Guide: SF_WEBAPP_DEV_GUIDE.md + - Complete Guide: SF_UI_BUNDLE_DEV_GUIDE.md ``` diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_UI_BUNDLE_DEV_GUIDE.md similarity index 76% rename from SF_WEBAPP_DEV_GUIDE.md rename to SF_UI_BUNDLE_DEV_GUIDE.md index 4095bce..8d88442 100644 --- a/SF_WEBAPP_DEV_GUIDE.md +++ b/SF_UI_BUNDLE_DEV_GUIDE.md @@ -1,4 +1,4 @@ -# Salesforce Webapp Dev Command Guide +# Salesforce Multi-Framework Dev Command Guide > **Develop web applications with seamless Salesforce integration** @@ -6,11 +6,11 @@ ## Overview -The `sf webapp dev` command enables local development of modern web applications (React, Vue, Angular, etc.) with automatic Salesforce authentication. It intelligently discovers your webapp configuration, handles proxy routing, injects authentication headers, and supports hot reload - so you can focus on building your app. +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 webapp configuration, handles proxy routing, injects authentication headers, and supports hot reload - so you can focus on building your app. ### Key Features -- **Auto-Discovery**: Automatically finds webapps in `webapplications/` folder +- **Auto-Discovery**: Automatically finds webapps in `webui/` folder - **Optional Manifest**: `webapplication.json` is optional - uses sensible defaults - **Auto-Selection**: Automatically selects webapp when running from inside its folder - **Interactive Selection**: Prompts with arrow-key navigation when multiple webapps exist @@ -29,7 +29,7 @@ The `sf webapp dev` command enables local development of modern web applications ``` my-sfdx-project/ β”œβ”€β”€ sfdx-project.json -└── force-app/main/default/webapplications/ +└── force-app/main/default/webui/ └── my-app/ β”œβ”€β”€ my-app.webapplication-meta.xml β”œβ”€β”€ package.json @@ -40,7 +40,7 @@ my-sfdx-project/ ### 2. Run the command ```bash -sf webapp dev --target-org myOrg --open +sf ui-bundle dev --target-org myOrg --open ``` ### 3. Start developing @@ -60,39 +60,39 @@ Browser opens to `http://localhost:4545` with your app running and Salesforce au ## Command Syntax ```bash -sf webapp dev [OPTIONS] +sf ui-bundle dev [OPTIONS] ``` ### Options -| Option | Short | Description | Default | -| -------------- | ----- | ----------------------------------------------- | ------------- | -| `--target-org` | `-o` | Salesforce org alias or username | Required | -| `--name` | `-n` | Web application 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 | +| Option | Short | Description | Default | +| -------------- | ----- | ---------------------------------- | ------------- | +| `--target-org` | `-o` | Salesforce org alias or username | Required | +| `--name` | `-n` | Web application 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 | ### Examples ```bash # Simplest - auto-discovers webapplication.json -sf webapp dev --target-org myOrg +sf ui-bundle dev --target-org myOrg # With browser auto-open -sf webapp dev --target-org myOrg --open +sf ui-bundle dev --target-org myOrg --open # Specify webapp by name (when multiple exist) -sf webapp dev --name myApp --target-org myOrg +sf ui-bundle dev --name myApp --target-org myOrg # Custom port -sf webapp dev --target-org myOrg --port 8080 +sf ui-bundle dev --target-org myOrg --port 8080 # Explicit dev server URL (skip auto-detection) -sf webapp dev --target-org myOrg --url http://localhost:5173 +sf ui-bundle dev --target-org myOrg --url http://localhost:5173 # Debug mode -SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg +SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrg ``` --- @@ -105,7 +105,7 @@ The command discovers webapps using a simplified, deterministic algorithm. Webap ```mermaid flowchart TD - Start["sf webapp dev"] --> CheckInside{"Inside webapplications/
webapp folder?"} + Start["sf ui-bundle dev"] --> CheckInside{"Inside webui/
webapp folder?"} CheckInside -->|Yes| HasNameInside{"--name provided?"} HasNameInside -->|Yes, different| ErrorConflict["Error: --name conflicts
with current directory"] @@ -113,7 +113,7 @@ flowchart TD CheckInside -->|No| CheckSFDX{"In SFDX project?
(sfdx-project.json)"} - CheckSFDX -->|Yes| CheckPath["Check force-app/main/
default/webapplications/"] + CheckSFDX -->|Yes| CheckPath["Check force-app/main/
default/webui/"] CheckPath --> HasName{"--name provided?"} CheckSFDX -->|No| CheckMetaXml{"Current dir has
.webapplication-meta.xml?"} @@ -148,7 +148,7 @@ flowchart TD my-sfdx-project/ β”œβ”€β”€ sfdx-project.json # SFDX project marker └── force-app/main/default/ - └── webapplications/ # Standard SFDX location + └── webui/ # Standard SFDX location β”œβ”€β”€ app-one/ # Webapp 1 (with dev config) β”‚ β”œβ”€β”€ app-one.webapplication-meta.xml # Required: identifies as webapp β”‚ β”œβ”€β”€ webapplication.json # Optional: dev configuration @@ -164,8 +164,8 @@ my-sfdx-project/ The command uses a simplified, deterministic approach: -1. **Inside webapp folder**: If running from `webapplications//` or deeper, auto-selects that webapp -2. **SFDX project root**: Uses fixed path `force-app/main/default/webapplications/` +1. **Inside webapp folder**: If running from `webui//` or deeper, auto-selects that webapp +2. **SFDX project root**: Uses fixed path `force-app/main/default/webui/` 3. **Standalone**: If current directory has a `.webapplication-meta.xml` file, uses it directly **Important**: Only directories containing a `{name}.webapplication-meta.xml` file are recognized as valid webapps. @@ -177,9 +177,9 @@ When multiple webapps are found, you'll see an interactive prompt: ``` Found 3 webapps in project ? Select the webapp to run: (Use arrow keys) -❯ app-one (webapplications/app-one) - app-two (webapplications/app-two) [no manifest] - app-three (webapplications/app-three) +❯ app-one (webui/app-one) + app-two (webui/app-two) [no manifest] + app-three (webui/app-three) ``` Format: @@ -238,10 +238,10 @@ 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. | +| 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) @@ -251,9 +251,9 @@ The `webapplication.json` file is **optional**. All fields are also optional - m #### Dev Configuration -| 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` | +| 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):** @@ -306,13 +306,13 @@ The `webapplication.json` file is **optional**. All fields are also optional - m ### Example: Minimal (No Manifest) ``` -webapplications/ +webui/ └── my-dashboard/ β”œβ”€β”€ package.json # Has "scripts": { "dev": "vite" } └── src/ ``` -Run: `sf webapp dev --target-org myOrg` +Run: `sf ui-bundle dev --target-org myOrg` Console output: @@ -325,7 +325,7 @@ Warning: No webapplication.json found for webapp "my-dashboard" β†’ Manifest watching: disabled πŸ’‘ To customize, create a webapplication.json file in your webapp directory. -βœ… Using webapp: my-dashboard (webapplications/my-dashboard) +βœ… Using webapp: my-dashboard (webui/my-dashboard) βœ… Ready for development! β†’ Proxy: http://localhost:4545 (open this in your browser) @@ -390,13 +390,13 @@ Automatically detects Salesforce Code Builder environment and binds to `0.0.0.0` 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 | +| 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 | ### Connect to Existing Dev Server (Proxy-Only Mode) @@ -409,7 +409,7 @@ npm run dev # Output: Local: http://localhost:5173/ # Terminal 2: Connect proxy to your running server -sf webapp dev --url http://localhost:5173 --target-org myOrg +sf ui-bundle dev --url http://localhost:5173 --target-org myOrg ``` **Output:** @@ -425,7 +425,7 @@ sf webapp dev --url http://localhost:5173 --target-org myOrg When using `dev.command`, the default URL is `http://localhost:5173`. Override with `--url` if your dev server uses a different port: ```bash -sf webapp dev --url http://localhost:3000 --target-org myOrg +sf ui-bundle dev --url http://localhost:3000 --target-org myOrg ``` If the URL is not reachable, the CLI starts the dev server and uses the actual URL (with a warning if it differs). @@ -439,7 +439,7 @@ If the URL is not reachable, the CLI starts the dev server and uses the actual U Ensure your webapp has the required `.webapplication-meta.xml` file: ``` -force-app/main/default/webapplications/ +force-app/main/default/webui/ └── my-app/ β”œβ”€β”€ my-app.webapplication-meta.xml # Required! β”œβ”€β”€ package.json @@ -454,8 +454,8 @@ This error occurs when you're inside one webapp folder but try to run a differen ```bash # You're in FirstWebApp folder but trying to run SecondWebApp -cd webapplications/FirstWebApp -sf webapp dev --name SecondWebApp --target-org myOrg # Error! +cd webui/FirstWebApp +sf ui-bundle dev --name SecondWebApp --target-org myOrg # Error! ``` **Solutions:** @@ -470,7 +470,7 @@ The `--name` flag matches the folder name of the webapp. ```bash # This looks for webapp named "myApp" -sf webapp dev --name myApp --target-org myOrg +sf ui-bundle dev --name myApp --target-org myOrg ``` ### "Dependencies Not Installed" / "command not found" @@ -478,7 +478,7 @@ sf webapp dev --name myApp --target-org myOrg Install dependencies in your webapp folder: ```bash -cd webapplications/my-app +cd webui/my-app npm install ``` @@ -486,13 +486,13 @@ npm install 1. Ensure dev server is running: `npm run dev` 2. Verify URL in `webapplication.json` is correct -3. Try explicit URL: `sf webapp dev --url http://localhost:5173 --target-org myOrg` +3. Try explicit URL: `sf ui-bundle dev --url http://localhost:5173 --target-org myOrg` ### "Port 4545 already in use" ```bash # Use a different port -sf webapp dev --port 8080 --target-org myOrg +sf ui-bundle dev --port 8080 --target-org myOrg # Or find and kill the process using the port lsof -i :4545 @@ -524,14 +524,14 @@ tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev | jq -r **Step 2: Run command in Terminal 2** ```bash -SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg +SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrg ``` **Example debug output:** ``` Discovering webapplication.json manifest(s)... -Using webapp: myApp at webapplications/my-app +Using webapp: myApp at webui/my-app Manifest loaded: myApp Starting dev server with command: npm run dev Dev server ready at: http://localhost:5173/ @@ -548,7 +548,7 @@ The command integrates with the Salesforce VSCode UI Preview extension (`salesfo 1. Extension detects `webapplication.json` in workspace 2. User clicks "Preview" button on the file -3. Extension executes: `sf webapp dev --target-org --open` +3. Extension executes: `sf ui-bundle dev --target-org --open` 4. If multiple webapps exist, uses `--name` to specify which one 5. Browser opens with the app running @@ -559,7 +559,7 @@ The command integrates with the Salesforce VSCode UI Preview extension (`salesfo For scripting and CI/CD, use the `--json` flag: ```bash -sf webapp dev --target-org myOrg --json +sf ui-bundle dev --target-org myOrg --json ``` Output: @@ -581,7 +581,7 @@ Output: ### Building the Plugin ```bash -cd /path/to/plugin-app-dev +cd /path/to/plugin-ui-bundle-dev # Install dependencies yarn install @@ -605,7 +605,7 @@ yarn build # Rebuild - no re-linking needed ### Project Structure ``` -plugin-app-dev/ +plugin-ui-bundle-dev/ β”œβ”€β”€ src/ β”‚ β”œβ”€β”€ commands/webapp/ β”‚ β”‚ └── dev.ts # Main command implementation @@ -649,4 +649,4 @@ plugin-app-dev/ --- -**Repository:** [github.com/salesforcecli/plugin-app-dev](https://github.com/salesforcecli/plugin-app-dev) +**Repository:** [github.com/salesforcecli/plugin-ui-bundle-dev](https://github.com/salesforcecli/plugin-ui-bundle-dev) diff --git a/command-snapshot.json b/command-snapshot.json index 44c13e3..3bdb1a4 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,10 +1,10 @@ [ { "alias": [], - "command": "webapp:dev", + "command": "ui-bundle:dev", "flagAliases": [], "flagChars": ["b", "n", "o", "p", "u"], "flags": ["flags-dir", "json", "name", "open", "port", "target-org", "url"], - "plugin": "@salesforce/plugin-app-dev" + "plugin": "@salesforce/plugin-ui-bundle-dev" } ] diff --git a/messages/ui-bundle.dev.md b/messages/ui-bundle.dev.md new file mode 100644 index 0000000..83f2cae --- /dev/null +++ b/messages/ui-bundle.dev.md @@ -0,0 +1,247 @@ +# summary + +Preview a web application locally and in real-time, without deploying it to your org. + +# description + +This command starts a local development (dev) server so you can preview a web application using the local metadata files in your DX project. Using a local preview helps you quickly develop web applications, because you don't have to continually deploy metadata to your org. + +The command also launches a local proxy server that sits between your web application and Salesforce, automatically injecting authentication headers from Salesforce CLI's stored tokens. The proxy allows your web app to make authenticated API calls to Salesforce without exposing credentials. + +Even though you're previewing the web application locally and not deploying anything to an org, you're still required to authorize and specify an org to use this command. + +Salesforce web applications are represented by the WebApplication metadata type. + +# flags.name.summary + +Name of the web application to preview. + +# flags.name.description + +The unique name of the web application, as defined by the "name" property in the webapplication.json runtime configuration file. The webapplication.json file is located in the "uiBundles" metadata directory of your DX project, such as force-app/main/default/uiBundles/MyApp/webapplication.json. + +If you don't specify this flag, the command automatically discovers the webapplication.json files in the current directory and subdirectories. If the command finds only one webapplication.json, it automatically uses it. If it finds multiple files, the command prompts you to select one. + +# flags.url.summary + +URL where your developer server runs, such as https://localhost:5173. All UI, static, and hot deployment requests are forwarded to this URL. + +# flags.url.description + +You must specify this flag if the web application's webapplication.json file doesn't contain a value for either the "dev.command" or "dev.url" configuration properties. All non-Salesforce API requests are forwarded to this URL. + +If you specify this flag, it overrides the value in the webapplication.json file. + +This is the order of precedence that the dev server uses for the URL: --url flag > manifest dev.url > URL from the dev server process (which was started using either manifest dev.command or default npm run dev). + +# flags.port.summary + +Local port where the proxy server listens. + +# flags.port.description + +Be sure your browser connects to this port, and not directly to the dev server. The proxy then forwards authenticated requests to Salesforce and other requests to your local dev server. + +# flags.open.summary + +Automatically open the proxy server URL in your default browser when the dev server is ready. + +# flags.open.description + +This flag saves you from manually copying and pasting the URL. The browser opens to the proxy URL, and not the dev server URL directly, which ensures that all requests are property authenticated. + +# examples + +- Start the local development (dev) server by automatically discovering the web application's webapplication.json file; use the org with alias "myorg": + + <%= config.bin %> <%= command.id %> --target-org myorg + +- Start the dev server by explicitly specifying the web application's name: + + <%= config.bin %> <%= command.id %> --name myWebApp --target-org myorg + +- Start at the specified dev server URL: + + <%= config.bin %> <%= command.id %> --name myWebApp --url http://localhost:5173 --target-org myorg + +- Start with a custom proxy port and automatically open the proxy server URL in your browser: + + <%= config.bin %> <%= command.id %> --target-org myorg --port 4546 --open + +- Start with debug logging enabled by specifing the SF_LOG_LEVEL environment variable before running the command: + + SF_LOG_LEVEL=debug <%= config.bin %> <%= command.id %> --target-org myorg + +# info.manifest-changed + +Manifest %s detected. + +# info.manifest-reloaded + +βœ“ Manifest reloaded successfully. + +# info.dev-url-changed + +Dev server URL updated to: %s. + +# info.dev-server-url + +Dev server URL: %s. + +# info.proxy-url + +Proxy URL: %s (open this URL in your browser). + +# info.ready-for-development + +βœ… Ready for development! + β†’ %s (open this URL in your browser). + +# info.ready-for-development-vite + +βœ… Ready for development! + β†’ %s (Vite proxy active - open this URL in your browser). + +# info.press-ctrl-c + +Press Ctrl+C to stop. + +# info.press-ctrl-c-target + +Press Ctrl+C to stop the %s. + +# info.stopped-target + +βœ… Stopped %s. + +# info.stop-target-dev + +dev server + +# info.stop-target-proxy + +proxy server + +# info.stop-target-both + +dev and proxy servers + +# info.server-running + +Dev server is running. Stop it by running "SFDX: Close Live Preview" from the VS Code command palette. + +# info.server-running-target-dev + +Dev server is running. Stop it by running "SFDX: Close Live Preview" from the VS Code command palette. + +# info.server-running-target-proxy + +Proxy server is running. Stop it by running "SFDX: Close Live Preview" from the VS Code command palette. + +# info.server-running-target-both + +Dev and proxy servers are running. Stop them by running "SFDX: Close Live Preview" from the VS Code command palette. + +# info.dev-server-healthy + +βœ“ Dev server is responding at: %s. + +# info.dev-server-detected + +βœ… Dev server detected at %s. + +# info.start-dev-server-hint + +Start your dev server to continue development. + +# warning.dev-server-not-responding + +⚠ Dev server returned status %s from: %s. + +# warning.dev-server-unreachable + +⚠ Dev server is not responding at: %s. + +# warning.dev-server-unreachable-status + +⚠️ Dev server unreachable at %s. + +# warning.dev-server-start-hint + +The proxy server is running, but the dev server may not be started yet. +Make sure to start your dev server (e.g., 'npm run dev') before opening the browser. + +# warning.dev-command-changed + +dev.command changed to "%s" - restart the command to apply this change. + +# error.manifest-watch-failed + +Failed to watch manifest: %s. + +# error.dev-url-unreachable + +Dev server unreachable at %s. +Start your dev server manually at that URL, or add dev.command to webapplication.json to start it automatically. + +# error.dev-url-unreachable-with-flag + +Dev server unreachable at %s. +Remove --url to use dev.command to start the server automatically, or ensure your dev server is running at that URL. + +# error.port-in-use + +Port %s is already in use. Try specifying a different port with the --port flag or stopping the service that's using the port. + +# error.dev-server-failed + +%s + +# info.multiple-webapps-found + +Found %s webapps in project. + +# info.webapp-auto-selected + +Auto-selected webapp "%s" (running from inside its folder). + +# info.using-webapp + +βœ… Using webapp: %s (%s). + +# info.starting-webapp + +βœ… Starting %s. + +# prompt.select-webapp + +Select the webapp to run: + +# info.no-manifest-defaults + +No webapplication.json found. Using defaults: dev command=%s, proxy port=%s. + +Tip: See "sf ui-bundle dev --help" for configuration options. + +# warning.empty-manifest + +No dev configuration in webapplication.json - using defaults (command: %s). + +Tip: See "sf ui-bundle dev --help" for configuration options. + +# info.using-defaults + +Using default dev command: %s. + +# info.url-already-available + +βœ… URL %s is already available, skipping dev server startup (proxy-only mode). + +# warning.url-mismatch + +⚠️ The --url flag (%s) does not match the actual dev server URL (%s). +The proxy will use the actual dev server URL. + +# info.vite-proxy-detected + +Vite WebApp proxy detected at %s - using Vite's built-in proxy (standalone proxy skipped). diff --git a/messages/webapp.dev.md b/messages/webapp.dev.md deleted file mode 100644 index 5d8f6a0..0000000 --- a/messages/webapp.dev.md +++ /dev/null @@ -1,240 +0,0 @@ -# summary - -Preview a web app locally without needing to deploy - -# description - -Starts a local development server for a Web Application, using the local project files. This enables rapid development with hot reloading and immediate feedback. - -The command launches a local proxy server that sits between your web application and Salesforce, automatically injecting authentication headers from the CLI's stored tokens. This allows your web app to make authenticated API calls to Salesforce without exposing credentials. - -# flags.name.summary - -Identifies the Web Application (optional) - -# flags.name.description - -The unique name of the web application as defined in webapplication.json. If not provided, the command will automatically discover webapplication.json files in the current directory and subdirectories. If only one webapplication.json is found, it will be used automatically. If multiple are found, you will be prompted to select one. - -# flags.url.summary - -Dev server origin to forward UI/HMR/static requests - -# flags.url.description - -The URL where your dev server is running (e.g., http://localhost:5173). Required if webapplication.json does not contain a dev.command or dev.url configuration. All non-Salesforce API requests will be forwarded to this URL. - -Dev server URL precedence: --url flag > manifest dev.url > URL from dev server process (started via manifest dev.command or default npm run dev). - -# flags.port.summary - -Local proxy port - -# flags.port.description - -The port on which the proxy server will listen. Your browser should connect to this port, not directly to the dev server. The proxy will forward authenticated requests to Salesforce and other requests to your dev server. - -# flags.open.summary - -Auto-open proxy URL in default browser - -# flags.open.description - -Automatically opens the proxy server URL in your default web browser when the server is ready. This saves you from manually copying and pasting the URL. The browser will open to the proxy URL (not the dev server URL directly), ensuring all requests are properly authenticated. - -# examples - -- Start the development server (auto-discovers webapplication.json): - - <%= config.bin %> <%= command.id %> --target-org myorg - -- Start the development server with explicit webapp name: - - <%= config.bin %> <%= command.id %> --name myWebApp --target-org myorg - -- Start the development server with explicit dev server URL: - - <%= config.bin %> <%= command.id %> --name myWebApp --url http://localhost:5173 --target-org myorg - -- Start with custom port and auto-open browser: - - <%= config.bin %> <%= command.id %> --target-org myorg --port 4546 --open - -- Start with debug logging (using SF_LOG_LEVEL environment variable): - - SF_LOG_LEVEL=debug <%= config.bin %> <%= command.id %> --target-org myorg - -# info.manifest-changed - -Manifest %s detected - -# info.manifest-reloaded - -βœ“ Manifest reloaded successfully - -# info.dev-url-changed - -Dev server URL updated to: %s - -# info.dev-server-url - -Dev server URL: %s - -# info.proxy-url - -Proxy URL: %s (open this URL in your browser) - -# info.ready-for-development - -βœ… Ready for development! - β†’ %s (open this URL in your browser) - -# info.ready-for-development-vite - -βœ… Ready for development! - β†’ %s (Vite proxy active - open this URL in your browser) - -# info.press-ctrl-c - -Press Ctrl+C to stop. - -# info.press-ctrl-c-target - -Press Ctrl+C to stop the %s. - -# info.stopped-target - -βœ… Stopped %s. - -# info.stop-target-dev - -dev server - -# info.stop-target-proxy - -proxy server - -# info.stop-target-both - -dev and proxy servers - -# info.server-running - -Dev server is running. Stop it by running "SFDX: Close Live Preview" from the VS Code command palette. - -# info.server-running-target-dev - -Dev server is running. Stop it by running "SFDX: Close Live Preview" from the VS Code command palette. - -# info.server-running-target-proxy - -Proxy server is running. Stop it by running "SFDX: Close Live Preview" from the VS Code command palette. - -# info.server-running-target-both - -Dev and proxy servers are running. Stop them by running "SFDX: Close Live Preview" from the VS Code command palette. - -# info.dev-server-healthy - -βœ“ Dev server is responding at: %s - -# info.dev-server-detected - -βœ… Dev server detected at %s - -# info.start-dev-server-hint - -Start your dev server to continue development - -# warning.dev-server-not-responding - -⚠ Dev server returned status %s from: %s - -# warning.dev-server-unreachable - -⚠ Dev server is not responding at: %s - -# warning.dev-server-unreachable-status - -⚠️ Dev server unreachable at %s - -# warning.dev-server-start-hint - -The proxy server is running, but the dev server may not be started yet. -Make sure to start your dev server (e.g., 'npm run dev') before opening the browser. - -# warning.dev-command-changed - -dev.command changed to "%s" - restart the command to apply this change. - -# error.manifest-watch-failed - -Failed to watch manifest: %s - -# error.dev-url-unreachable - -Dev server unreachable at %s. -Start your dev server manually at that URL, or add dev.command to webapplication.json to start it automatically. - -# error.dev-url-unreachable-with-flag - -Dev server unreachable at %s. -Remove --url to use dev.command to start the server automatically, or ensure your dev server is running at that URL. - -# error.port-in-use - -Port %s is already in use. Try specifying a different port with the --port flag or stopping the service that's using the port. - -# error.dev-server-failed - -%s - -# info.multiple-webapps-found - -Found %s webapps in project - -# info.webapp-auto-selected - -Auto-selected webapp "%s" (running from inside its folder) - -# info.using-webapp - -βœ… Using webapp: %s (%s) - -# info.starting-webapp - -βœ… Starting %s - -# prompt.select-webapp - -Select the webapp to run: - -# info.no-manifest-defaults - -No webapplication.json found. Using defaults: dev command=%s, proxy port=%s - -Tip: See "sf webapp dev --help" for configuration options. - -# warning.empty-manifest - -No dev configuration in webapplication.json - using defaults (command: %s) - -Tip: See "sf webapp dev --help" for configuration options. - -# info.using-defaults - -Using default dev command: %s - -# info.url-already-available - -βœ… URL %s is already available, skipping dev server startup (proxy-only mode) - -# warning.url-mismatch - -⚠️ The --url flag (%s) does not match the actual dev server URL (%s). -The proxy will use the actual dev server URL. - -# info.vite-proxy-detected - -Vite WebApp proxy detected at %s - using Vite's built-in proxy (standalone proxy skipped) - diff --git a/package.json b/package.json index 3b6217d..f4e1caa 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@salesforce/plugin-app-dev", + "name": "@salesforce/plugin-ui-bundle-dev", "description": "A CLI plugin for building UI that integrates with Salesforce", "version": "1.2.0", "author": "Salesforce", @@ -41,7 +41,7 @@ "/oclif.manifest.json", "/schemas" ], - "homepage": "https://github.com/salesforcecli/plugin-app-dev", + "homepage": "https://github.com/salesforcecli/plugin-ui-bundle-dev", "keywords": [ "force", "salesforce", @@ -62,13 +62,13 @@ "@salesforce/plugin-command-reference" ], "topics": { - "webapp": { - "description": "Work with Salesforce Web Apps" + "ui-bundle": { + "description": "Work with UI bundles" } }, "flexibleTaxonomy": true }, - "repository": "salesforcecli/plugin-app-dev", + "repository": "salesforcecli/plugin-ui-bundle-dev", "scripts": { "build": "wireit", "clean": "sf-clean", diff --git a/schemas/webapp-dev.json b/schemas/ui__bundle-dev.json similarity index 74% rename from schemas/webapp-dev.json rename to schemas/ui__bundle-dev.json index b3ee114..f55d1e0 100644 --- a/schemas/webapp-dev.json +++ b/schemas/ui__bundle-dev.json @@ -14,9 +14,12 @@ "description": "Dev server URL being proxied" } }, - "required": ["url", "devServerUrl"], + "required": [ + "url", + "devServerUrl" + ], "additionalProperties": false, - "description": "Command execution result What the sf webapp dev command returns to the user" + "description": "Command execution result What the sf ui-bundle dev command returns to the user" } } -} +} \ No newline at end of file diff --git a/src/commands/webapp/dev.ts b/src/commands/ui-bundle/dev.ts similarity index 96% rename from src/commands/webapp/dev.ts rename to src/commands/ui-bundle/dev.ts index 2cc476b..41ee789 100644 --- a/src/commands/webapp/dev.ts +++ b/src/commands/ui-bundle/dev.ts @@ -26,9 +26,9 @@ import { ProxyServer } from '../../proxy/ProxyServer.js'; import { discoverWebapp, DEFAULT_DEV_COMMAND, type DiscoveredWebapp } from '../../config/webappDiscovery.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-app-dev', 'webapp.dev'); +const messages = Messages.loadMessages('@salesforce/plugin-ui-bundle-dev', 'ui-bundle.dev'); -export default class WebappDev extends SfCommand { +export default class UiBundleDev extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); @@ -77,7 +77,7 @@ export default class WebappDev extends SfCommand { * Prompt user to select a webapp from multiple discovered webapps * Uses interactive arrow-key selection (standard SF CLI pattern) */ - private static async promptWebappSelection(webapps: DiscoveredWebapp[]): Promise { + private static async promptUiBundleSelection(webapps: DiscoveredWebapp[]): Promise { const WARNING = '\u26A0\uFE0F'; // ⚠️ const choices = webapps.map((webapp) => { @@ -133,14 +133,14 @@ export default class WebappDev extends SfCommand { intervalMs = 500, start = Date.now() ): Promise { - if (await WebappDev.isUrlReachable(url)) { + if (await UiBundleDev.isUrlReachable(url)) { return true; } if (Date.now() - start >= timeoutMs) { return false; } await new Promise((r) => setTimeout(r, intervalMs)); - return WebappDev.pollUntilReachable(url, timeoutMs, intervalMs, start); + return UiBundleDev.pollUntilReachable(url, timeoutMs, intervalMs, start); } /** @@ -169,11 +169,11 @@ export default class WebappDev extends SfCommand { // eslint-disable-next-line complexity public async run(): Promise { - const { flags } = await this.parse(WebappDev); + const { flags } = await this.parse(UiBundleDev); // Initialize logger from @salesforce/core for debug logging // Logger respects SF_LOG_LEVEL environment variable - this.logger = await Logger.child('WebappDev'); + this.logger = await Logger.child('UiBundleDev'); // Declare variables outside try block for catch block access let manifest: WebAppManifest | null = null; @@ -191,7 +191,7 @@ export default class WebappDev extends SfCommand { if (!discoveredWebapp) { this.log(messages.getMessage('info.multiple-webapps-found', [String(allWebapps.length)])); - selectedWebapp = await WebappDev.promptWebappSelection(allWebapps); + selectedWebapp = await UiBundleDev.promptUiBundleSelection(allWebapps); } else { selectedWebapp = discoveredWebapp; @@ -283,7 +283,7 @@ export default class WebappDev extends SfCommand { } // Check if URL is already reachable - const isReachable = await WebappDev.isUrlReachable(resolvedUrl); + const isReachable = await UiBundleDev.isUrlReachable(resolvedUrl); if (isReachable) { devServerUrl = resolvedUrl; this.log(messages.getMessage('info.url-already-available', [resolvedUrl])); @@ -344,7 +344,7 @@ export default class WebappDev extends SfCommand { this.devServerManager.start(); // Poll until URL is reachable, or fail immediately on process error - const pollPromise = WebappDev.pollUntilReachable(resolvedUrl, 60_000); + const pollPromise = UiBundleDev.pollUntilReachable(resolvedUrl, 60_000); const errorPromise = new Promise((_, reject) => { this.devServerManager!.once('error', (error: SfError | DevServerError) => { const devError = @@ -410,7 +410,7 @@ export default class WebappDev extends SfCommand { // Step 5: Check for Vite proxy and conditionally start standalone proxy this.logger.debug('Checking if Vite WebApp proxy is active...'); - const viteProxyActive = await WebappDev.checkViteProxyActive(devServerUrl); + const viteProxyActive = await UiBundleDev.checkViteProxyActive(devServerUrl); // Track the final URL to open in browser (either proxy or dev server) let finalUrl: string; @@ -489,7 +489,7 @@ export default class WebappDev extends SfCommand { // Step 7: Open browser if requested if (flags.open) { this.logger.debug('Opening browser...'); - await WebappDev.openBrowser(finalUrl); + await UiBundleDev.openBrowser(finalUrl); } // Display usage instructions @@ -577,7 +577,7 @@ export default class WebappDev extends SfCommand { // Wrap unknown errors const errorMessage = error instanceof Error ? error.message : String(error); - throw new SfError(`❌ Failed to start webapp dev command: ${errorMessage}`, 'UnexpectedError', [ + throw new SfError(`❌ Failed to start ui-bundle dev command: ${errorMessage}`, 'UnexpectedError', [ 'This is an unexpected error', 'Please try again', 'If the problem persists, check the command logs with SF_LOG_LEVEL=debug', diff --git a/src/config/types.ts b/src/config/types.ts index 77ac97b..260b2d9 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -22,7 +22,7 @@ export type { ManifestChangeEvent } from './ManifestWatcher.js'; /** * Command execution result - * What the sf webapp dev command returns to the user + * What the sf ui-bundle dev command returns to the user */ export type WebAppDevResult = { /** Proxy server URL (where user should open browser) */ diff --git a/src/config/webappDiscovery.ts b/src/config/webappDiscovery.ts index 88d3ef9..f3ab702 100644 --- a/src/config/webappDiscovery.ts +++ b/src/config/webappDiscovery.ts @@ -27,10 +27,10 @@ const logger = Logger.childFromRoot('WebappDiscovery'); export const DEFAULT_DEV_COMMAND = 'npm run dev'; /** - * Standard metadata path segment for webapplications (relative to package directory). - * Consistent with other metadata types: packagePath/main/default/webapplications + * Standard metadata path segment for uiBundles (relative to package directory). + * Consistent with other metadata types: packagePath/main/default/uiBundles */ -const WEBAPPLICATIONS_RELATIVE_PATH = 'main/default/webapplications'; +const WEBAPPLICATIONS_RELATIVE_PATH = 'main/default/uiBundles'; /** * Pattern to match webapplication metadata XML files @@ -58,7 +58,7 @@ export type DiscoveredWebapp = { }; /** - * Directories to exclude when processing webapplications folder. + * Directories to exclude when processing uiBundles folder. * Note: Directories starting with '.' are excluded separately in shouldExcludeDirectory() */ const EXCLUDED_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'out', 'coverage', '__pycache__', 'venv']); @@ -70,14 +70,14 @@ function shouldExcludeDirectory(dirName: string): boolean { return EXCLUDED_DIRECTORIES.has(dirName) || dirName.startsWith('.'); } -/** Folder name for webapplications metadata */ -const WEBAPPLICATIONS_FOLDER = 'webapplications'; +/** Folder name for uiBundles metadata */ +export const UI_BUNDLES_FOLDER = 'uiBundles'; /** - * Check if a folder name is the standard webapplications folder + * Check if a folder name is the standard uiBundles folder */ function isWebapplicationsFolder(folderName: string): boolean { - return folderName === WEBAPPLICATIONS_FOLDER; + return folderName === UI_BUNDLES_FOLDER; } /** @@ -174,11 +174,11 @@ async function tryResolveSfdxProjectRoot(cwd: string): Promise { } /** - * Get all webapplications folder paths from the project's package directories. - * Consistent with other metadata types: each package can have main/default/webapplications. + * Get all uiBundles folder paths from the project's package directories. + * Consistent with other metadata types: each package can have main/default/uiBundles. * * @param projectRoot - Absolute path to project root (where sfdx-project.json lives) - * @returns Array of absolute paths to webapplications folders that exist + * @returns Array of absolute paths to uiBundles folders that exist */ async function getWebapplicationsPathsFromProject(projectRoot: string): Promise { try { @@ -199,24 +199,24 @@ async function getWebapplicationsPathsFromProject(projectRoot: string): Promise< } /** - * Check if we're inside a webapplications folder by traversing upward through parent directories. + * Check if we're inside a uiBundles folder by traversing upward through parent directories. * * This handles cases where the user runs the command from inside a webapp folder: * - * Example 1: Running from /project/force-app/main/default/webapplications/my-app/src/ - * Traverses: src -> my-app -> webapplications (found!) - * Returns: { webappsFolder: "/project/.../webapplications", currentWebappName: "my-app" } + * Example 1: Running from /project/force-app/main/default/uiBundles/my-app/src/ + * Traverses: src -> my-app -> uiBundles (found!) + * Returns: { webappsFolder: "/project/.../uiBundles", currentWebappName: "my-app" } * - * Example 2: Running from /project/force-app/main/default/webapplications/my-app/ - * Checks parent: webapplications (found!) - * Returns: { webappsFolder: "/project/.../webapplications", currentWebappName: "my-app" } + * Example 2: Running from /project/force-app/main/default/uiBundles/my-app/ + * Checks parent: uiBundles (found!) + * Returns: { webappsFolder: "/project/.../uiBundles", currentWebappName: "my-app" } * - * Example 3: Running from /project/force-app/main/default/webapplications/ - * Current dir is webapplications (found!) - * Returns: { webappsFolder: "/project/.../webapplications", currentWebappName: null } + * Example 3: Running from /project/force-app/main/default/uiBundles/ + * Current dir is uiBundles (found!) + * Returns: { webappsFolder: "/project/.../uiBundles", currentWebappName: null } * * @param dir - Directory to start from - * @returns Object with webapplications folder path and current webapp name, or null if not found + * @returns Object with uiBundles folder path and current webapp name, or null if not found */ function findWebapplicationsFolderUpward( dir: string @@ -226,13 +226,13 @@ function findWebapplicationsFolderUpward( const maxUpwardDepth = 10; let depth = 0; - // Walk up the directory tree looking for "webapplications" folder + // Walk up the directory tree looking for "uiBundles" folder while (depth < maxUpwardDepth) { const dirName = basename(currentDir); const parentDir = dirname(currentDir); - // Case: Current directory IS the webapplications folder - // e.g., cwd = /project/webapplications + // Case: Current directory IS the uiBundles folder + // e.g., cwd = /project/uiBundles if (isWebapplicationsFolder(dirName)) { return { webappsFolder: currentDir, @@ -240,8 +240,8 @@ function findWebapplicationsFolderUpward( }; } - // Case: Parent directory is the webapplications folder - // e.g., cwd = /project/webapplications/my-app (parent is webapplications) + // Case: Parent directory is the uiBundles folder + // e.g., cwd = /project/uiBundles/my-app (parent is webui) if (isWebapplicationsFolder(basename(parentDir))) { return { webappsFolder: parentDir, @@ -260,16 +260,16 @@ function findWebapplicationsFolderUpward( depth++; } - // Not inside a webapplications folder + // Not inside a uiBundles folder return null; } /** - * Discover all webapps inside the webapplications folder. + * Discover all webapps inside the uiBundles folder. * Only directories containing a {name}.webapplication-meta.xml file are considered valid webapps. * If a webapplication.json exists, use it for dev configuration. * - * @param webappsFolderPath - Absolute path to the webapplications folder + * @param webappsFolderPath - Absolute path to the uiBundles folder * @param cwd - Original working directory for relative path calculation * @returns Array of discovered webapps (only those with .webapplication-meta.xml) */ @@ -326,7 +326,7 @@ type FindAllWebappsResult = { webapps: DiscoveredWebapp[]; /** Name of webapp user is currently inside (folder name), null if not inside any */ currentWebappName: string | null; - /** Whether the webapplications folder was found (even if empty or no valid webapps) */ + /** Whether the uiBundles folder was found (even if empty or no valid webapps) */ webappsFolderFound: boolean; /** Whether we're in an SFDX project context */ inSfdxProject: boolean; @@ -336,8 +336,8 @@ type FindAllWebappsResult = { * Find all webapps using simplified discovery algorithm. * * Discovery strategy (in order): - * 1. Check if inside a webapplications/ directory (upward search) - * 2. Check for SFDX project and search webapplications in all package directories + * 1. Check if inside a uiBundles/ directory (upward search) + * 2. Check for SFDX project and search uiBundles in all package directories * 3. If neither, check if current directory is a webapp (has .webapplication-meta.xml) * * @param cwd - Directory to search from (defaults to process.cwd()) @@ -348,15 +348,15 @@ async function findAllWebapps(cwd: string = process.cwd()): Promise or webapplications//src/ + // Step 1: Check if we're inside a uiBundles folder (upward search) + // This handles: running from uiBundles/ or uiBundles//src/ const upwardResult = findWebapplicationsFolderUpward(cwd); if (upwardResult) { webappsFolder = upwardResult.webappsFolder; currentWebappName = upwardResult.currentWebappName; } else { - // Step 2: Check for SFDX project and search webapplications in all package directories + // Step 2: Check for SFDX project and search uiBundles in all package directories const projectRoot = await tryResolveSfdxProjectRoot(cwd); if (projectRoot) { @@ -378,7 +378,7 @@ async function findAllWebapps(cwd: string = process.cwd()): Promise directory: + * 2. Inside uiBundles/ directory: * - Auto-select current webapp * - Error if --name conflicts with current directory * @@ -470,21 +470,21 @@ export async function discoverWebapp( if (webappsFolderFound) { // Folder exists but no valid webapps (no .webapplication-meta.xml files) throw new SfError( - 'Found "webapplications" folder but no valid webapps inside it.\n' + + 'Found "uiBundles" folder but no valid webapps inside it.\n' + 'Each webapp must have a {name}.webapplication-meta.xml file.\n\n' + 'Expected structure:\n' + - ' webapplications/\n' + + ' uiBundles/\n' + ' └── my-app/\n' + ' β”œβ”€β”€ my-app.webapplication-meta.xml (required)\n' + ' └── webapplication.json (optional, for dev config)', 'WebappNotFoundError' ); } else if (inSfdxProject) { - // In SFDX project but webapplications folder doesn't exist + // In SFDX project but uiBundles folder doesn't exist throw new SfError( - 'No webapplications folder found in the SFDX project.\n\n' + + 'No uiBundles folder found in the SFDX project.\n\n' + 'Create the folder structure in any package directory (e.g. force-app, packages/my-pkg):\n' + - ' /main/default/webapplications/\n' + + ' /main/default/uiBundles/\n' + ' └── my-app/\n' + ' β”œβ”€β”€ my-app.webapplication-meta.xml (required)\n' + ' └── webapplication.json (optional, for dev config)', @@ -495,8 +495,8 @@ export async function discoverWebapp( throw new SfError( 'No webapp found.\n\n' + 'To use this command, either:\n' + - '1. Run from an SFDX project with webapps in /main/default/webapplications/\n' + - '2. Run from inside a webapplications// directory\n' + + '1. Run from an SFDX project with webapps in /main/default/uiBundles/\n' + + '2. Run from inside a uiBundles// directory\n' + '3. Run from a directory containing a {name}.webapplication-meta.xml file', 'WebappNotFoundError' ); diff --git a/src/proxy/ProxyServer.ts b/src/proxy/ProxyServer.ts index 3afd3d3..3a9adfe 100644 --- a/src/proxy/ProxyServer.ts +++ b/src/proxy/ProxyServer.ts @@ -120,24 +120,21 @@ export class ProxyServer extends EventEmitter { const chunks: Buffer[] = []; const routeScript = - ''; + ""; const wrapped = Object.create(res, { writeHead: { - value: ( - code: number, - h?: Record - ) => { + value: (code: number, h?: Record) => { statusCode = code; if (h) { const merged: Record = { ...headers, - ...h + ...h, }; headers = merged; } return true; - } + }, }, write: { value: ( @@ -154,19 +151,13 @@ export class ProxyServer extends EventEmitter { actualEncoding = encoding; actualCb = cb; } - chunks.push( - Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, actualEncoding as BufferEncoding) - ); + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, actualEncoding as BufferEncoding)); if (actualCb) actualCb(); return true; - } + }, }, end: { - value: ( - chunk?: Buffer | string | (() => void), - encoding?: BufferEncoding | (() => void), - cb?: () => void - ) => { + value: (chunk?: Buffer | string | (() => void), encoding?: BufferEncoding | (() => void), cb?: () => void) => { let actualChunk: Buffer | string | undefined; let actualEncoding: BufferEncoding | undefined; let actualCb: (() => void) | undefined; @@ -185,9 +176,7 @@ export class ProxyServer extends EventEmitter { } if (actualChunk) chunks.push( - Buffer.isBuffer(actualChunk) - ? actualChunk - : Buffer.from(actualChunk, actualEncoding as BufferEncoding) + Buffer.isBuffer(actualChunk) ? actualChunk : Buffer.from(actualChunk, actualEncoding as BufferEncoding) ); const body = Buffer.concat(chunks); const contentType = (headers['content-type'] ?? headers['Content-Type'] ?? '') as string; @@ -199,8 +188,8 @@ export class ProxyServer extends EventEmitter { res.writeHead(statusCode, headers); res.end(body, actualCb); } - } - } + }, + }, }) as ServerResponse; return wrapped; diff --git a/test/commands/webapp/_cleanup.nut.ts b/test/commands/ui-bundle/_cleanup.nut.ts similarity index 100% rename from test/commands/webapp/_cleanup.nut.ts rename to test/commands/ui-bundle/_cleanup.nut.ts diff --git a/test/commands/webapp/dev.nut.ts b/test/commands/ui-bundle/dev.nut.ts similarity index 83% rename from test/commands/webapp/dev.nut.ts rename to test/commands/ui-bundle/dev.nut.ts index b1cc3eb..a4f7fec 100644 --- a/test/commands/webapp/dev.nut.ts +++ b/test/commands/ui-bundle/dev.nut.ts @@ -29,6 +29,7 @@ import { webappPath, ensureSfCli, authOrgViaUrl, + REAL_HOME, } from './helpers/webappProjectUtils.js'; /* ------------------------------------------------------------------ * @@ -37,7 +38,7 @@ import { * Validates flag-level parse errors that fire before any org or * * filesystem interaction. No credentials needed; always runs. * * ------------------------------------------------------------------ */ -describe('webapp dev NUTs β€” Tier 1 (no auth)', () => { +describe('ui-bundle dev NUTs β€” Tier 1 (no auth)', () => { let session: TestSession; before(async () => { @@ -51,7 +52,7 @@ describe('webapp dev NUTs β€” Tier 1 (no auth)', () => { // --target-org is declared as Flags.requiredOrg(). Running without it // must fail at parse time with NoDefaultEnvError before any other logic. it('should require --target-org', () => { - const result = execCmd('webapp dev --json', { + const result = execCmd('ui-bundle dev --json', { ensureExitCode: 1, cwd: session.dir, }); @@ -70,7 +71,7 @@ describe('webapp dev NUTs β€” Tier 1 (no auth)', () => { * * * Requires TESTKIT_AUTH_URL. Fails when absent (tests are mandatory). * * ------------------------------------------------------------------ */ -describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { +describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { let session: TestSession; let targetOrg: string; @@ -92,11 +93,11 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { // ── Discovery errors ────────────────────────────────────────── - // Project has no webapplications folder at all β†’ WebappNotFoundError. + // Project has no uiBundles folder at all β†’ WebappNotFoundError. it('should error when no webapp found (project only, no webapps)', () => { const projectDir = createProject(session, 'noWebappProject'); - const result = execCmd(`webapp dev --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); @@ -108,7 +109,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { it('should error when --name does not match any webapp', () => { const projectDir = createProjectWithWebapp(session, 'nameNotFound', 'realApp'); - const result = execCmd(`webapp dev --name NonExistent --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --name NonExistent --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); @@ -120,11 +121,15 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { // Discovery treats this as ambiguous intent and rejects it. it('should error on --name conflict when inside a different webapp', () => { const projectDir = createProjectWithWebapp(session, 'nameConflict', 'appA'); - execSync('sf webapp generate --name appB', { cwd: projectDir, stdio: 'pipe' }); + execSync('sf ui-bundle generate --name appB', { + cwd: projectDir, + stdio: 'pipe', + env: { ...process.env, HOME: REAL_HOME, USERPROFILE: REAL_HOME }, + }); const cwdInsideAppA = webappPath(projectDir, 'appA'); - const result = execCmd(`webapp dev --name appB --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --name appB --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: cwdInsideAppA, }); @@ -132,12 +137,12 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { expect(result.jsonOutput?.name).to.equal('WebappNameConflictError'); }); - // webapplications/ folder exists but is empty β†’ WebappNotFoundError. - it('should error when webapplications folder is empty', () => { + // uiBundles/ folder exists but is empty β†’ WebappNotFoundError. + it('should error when uiBundles folder is empty', () => { const projectDir = createProject(session, 'emptyWebapps'); createEmptyWebappsDir(projectDir); - const result = execCmd(`webapp dev --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); @@ -145,12 +150,12 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { expect(result.jsonOutput?.name).to.equal('WebappNotFoundError'); }); - // webapplications/orphanApp/ exists but has no .webapplication-meta.xml β†’ not a valid webapp. + // uiBundles/orphanApp/ exists but has no .webapplication-meta.xml β†’ not a valid webapp. it('should error when webapp dir has no .webapplication-meta.xml', () => { const projectDir = createProject(session, 'noMeta'); createWebappDirWithoutMeta(projectDir, 'orphanApp'); - const result = execCmd(`webapp dev --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); @@ -173,7 +178,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { dev: { url: 'http://localhost:5181' }, }); - const result = execCmd(`webapp dev --name appA --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --name appA --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); @@ -192,7 +197,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { dev: { url: 'http://localhost:5183' }, }); - const result = execCmd(`webapp dev --name appB --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --name appB --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); @@ -202,7 +207,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { // ── Auto-selection ──────────────────────────────────────────── - // When cwd is inside webapplications/myApp/, discovery auto-selects that + // When cwd is inside uiBundles/myApp/, discovery auto-selects that // webapp without --name. The command proceeds past discovery and fails at // URL resolution (no dev server running) β€” confirming auto-select worked. it('should auto-select webapp when run from inside its directory', () => { @@ -217,7 +222,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { // No --name flag; cwd is inside the webapp directory. // Discovery auto-selects myApp, then the command fails at URL check // (nothing running on 5179). DevServerUrlError proves discovery succeeded. - const result = execCmd(`webapp dev --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: cwdInsideApp, }); @@ -225,7 +230,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { expect(result.jsonOutput?.name).to.equal('DevServerUrlError'); }); - // When multiple webapps exist and cwd is inside webapplications/appA/, + // When multiple webapps exist and cwd is inside uiBundles/appA/, // discovery auto-selects appA without prompting. Proceeds past discovery // and fails at URL resolution β€” confirming auto-select works with multiple. it('should auto-select webapp when run from inside its directory (multiple webapps)', () => { @@ -240,7 +245,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { const cwdInsideAppA = webappPath(projectDir, 'appA'); - const result = execCmd(`webapp dev --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: cwdInsideAppA, }); @@ -255,7 +260,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { it('should error when --url is unreachable', () => { const projectDir = createProjectWithWebapp(session, 'urlUnreachable', 'myApp'); - const result = execCmd(`webapp dev --name myApp --url http://localhost:5179 --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --name myApp --url http://localhost:5179 --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); @@ -272,7 +277,7 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { dev: { url: 'http://localhost:5179' }, }); - const result = execCmd(`webapp dev --name myApp --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --name myApp --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); @@ -290,15 +295,12 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { const projectDir = createProjectWithWebapp(session, 'noInstall', 'myApp'); const appDir = webappPath(projectDir, 'myApp'); - writeFileSync( - join(appDir, 'package.json'), - JSON.stringify({ name: 'test-webapp', scripts: { dev: 'vite' } }) - ); + writeFileSync(join(appDir, 'package.json'), JSON.stringify({ name: 'test-webapp', scripts: { dev: 'vite' } })); writeManifest(projectDir, 'myApp', { dev: { command: 'npm run dev' }, }); - const result = execCmd(`webapp dev --name myApp --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --name myApp --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); diff --git a/test/commands/webapp/dev.test.ts b/test/commands/ui-bundle/dev.test.ts similarity index 99% rename from test/commands/webapp/dev.test.ts rename to test/commands/ui-bundle/dev.test.ts index ceb9668..4c8db06 100644 --- a/test/commands/webapp/dev.test.ts +++ b/test/commands/ui-bundle/dev.test.ts @@ -19,7 +19,7 @@ import sinon from 'sinon'; import { TestContext } from '@salesforce/core/testSetup'; import type { WebAppManifest, WebAppDevResult } from '../../../src/config/types.js'; -describe('webapp:dev command integration', () => { +describe('ui-bundle:dev command integration', () => { const $$ = new TestContext(); afterEach(() => { diff --git a/test/commands/webapp/devPort.nut.ts b/test/commands/ui-bundle/devPort.nut.ts similarity index 98% rename from test/commands/webapp/devPort.nut.ts rename to test/commands/ui-bundle/devPort.nut.ts index 5cbff70..054358c 100644 --- a/test/commands/webapp/devPort.nut.ts +++ b/test/commands/ui-bundle/devPort.nut.ts @@ -42,7 +42,7 @@ import { const DEV_PORT = 18_910; const PROXY_PORT = 18_920; -describe('webapp dev NUTs β€” Tier 2 port handling', function () { +describe('ui-bundle dev NUTs β€” Tier 2 port handling', function () { this.timeout(SUITE_TIMEOUT); let session: TestSession; diff --git a/test/commands/webapp/devWithUrl.nut.ts b/test/commands/ui-bundle/devWithUrl.nut.ts similarity index 97% rename from test/commands/webapp/devWithUrl.nut.ts rename to test/commands/ui-bundle/devWithUrl.nut.ts index 5c6d0a6..df3740d 100644 --- a/test/commands/webapp/devWithUrl.nut.ts +++ b/test/commands/ui-bundle/devWithUrl.nut.ts @@ -50,7 +50,7 @@ const FULL_FLOW_PORT = 18_900; const PROXY_ONLY_PORT = 18_930; const VITE_PORT = 18_940; -describe('webapp dev NUTs β€” Tier 2 URL/proxy integration', function () { +describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { this.timeout(SUITE_TIMEOUT); let session: TestSession; @@ -237,10 +237,14 @@ describe('webapp dev NUTs β€” Tier 2 URL/proxy integration', function () { handle = await spawnWebappDev( [ - '--name', 'myApp', - '--url', `http://localhost:${PROXY_ONLY_PORT + 3}`, - '--port', String(customProxyPort), - '--target-org', targetOrg, + '--name', + 'myApp', + '--url', + `http://localhost:${PROXY_ONLY_PORT + 3}`, + '--port', + String(customProxyPort), + '--target-org', + targetOrg, ], { cwd: projectDir, timeout: SPAWN_TIMEOUT } ); diff --git a/test/commands/webapp/helpers/devServerUtils.ts b/test/commands/ui-bundle/helpers/devServerUtils.ts similarity index 96% rename from test/commands/webapp/helpers/devServerUtils.ts rename to test/commands/ui-bundle/helpers/devServerUtils.ts index ce161cf..40b7a1f 100644 --- a/test/commands/webapp/helpers/devServerUtils.ts +++ b/test/commands/ui-bundle/helpers/devServerUtils.ts @@ -50,7 +50,7 @@ export type WebappDevHandle = { }; /** - * Spawn `sf webapp dev` asynchronously and wait for the JSON URL line on stderr. + * Spawn `sf ui-bundle dev` asynchronously and wait for the JSON URL line on stderr. * * Uses `bin/dev.js` (same binary that `execCmd` uses) so we test the * local plugin code, not whatever is installed globally. @@ -59,7 +59,7 @@ export function spawnWebappDev(args: string[], options: { cwd: string; timeout?: const binDev = join(process.cwd(), 'bin', 'dev.js'); const proc = spawn( process.execPath, - ['--loader', 'ts-node/esm', '--no-warnings=ExperimentalWarning', binDev, 'webapp', 'dev', ...args], + ['--loader', 'ts-node/esm', '--no-warnings=ExperimentalWarning', binDev, 'ui-bundle', 'dev', ...args], { cwd: options.cwd, stdio: ['pipe', 'pipe', 'pipe'], @@ -140,7 +140,7 @@ export function spawnWebappDev(args: string[], options: { cwd: string; timeout?: proc.on('close', (code) => { clearTimeout(timeoutId); if (code !== null && code !== 0) { - reject(new Error(`webapp dev exited with code ${code}.\nstderr:\n${stderrData}`)); + reject(new Error(`ui-bundle dev exited with code ${code}.\nstderr:\n${stderrData}`)); } }); }); diff --git a/test/commands/webapp/helpers/webappProjectUtils.ts b/test/commands/ui-bundle/helpers/webappProjectUtils.ts similarity index 77% rename from test/commands/webapp/helpers/webappProjectUtils.ts rename to test/commands/ui-bundle/helpers/webappProjectUtils.ts index dee8dfa..20aca99 100644 --- a/test/commands/webapp/helpers/webappProjectUtils.ts +++ b/test/commands/ui-bundle/helpers/webappProjectUtils.ts @@ -16,19 +16,26 @@ import { execSync } from 'node:child_process'; import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { homedir, tmpdir } from 'node:os'; import { join } from 'node:path'; -import { tmpdir } from 'node:os'; import type { TestSession } from '@salesforce/cli-plugins-testkit'; +import { UI_BUNDLES_FOLDER } from '../../../../src/config/webappDiscovery.js'; /** - * Relative path from project root to the webapplications folder. - * Mirrors WEBAPPLICATIONS_RELATIVE_PATH in src/config/webappDiscovery.ts. + * Real home directory captured at module load, before TestSession overrides process.env.HOME. + * Used when running `sf ui-bundle generate` so the CLI finds linked plugin-templates + * (TestSession sets HOME to a temp dir, which hides linked plugins). */ -const WEBAPPS_PATH = join('force-app', 'main', 'default', 'webapplications'); +export const REAL_HOME = homedir(); /** - * Resolve the absolute path to a webapp directory within a project. - * If `webAppName` is omitted, returns the webapplications folder itself. + * Relative path from project root to the uiBundles folder. + */ +const WEBAPPS_PATH = join('force-app', 'main', 'default', UI_BUNDLES_FOLDER); + +/** + * Resolve the absolute path to a UI bundle directory within a project. + * If `webAppName` is omitted, returns the uiBundles folder itself. */ export function webappPath(projectDir: string, webAppName?: string): string { return webAppName ? join(projectDir, WEBAPPS_PATH, webAppName) : join(projectDir, WEBAPPS_PATH); @@ -91,21 +98,22 @@ export function createProject(session: TestSession, name: string): string { } /** - * Run `sf project generate` then `sf webapp generate --name ` inside + * Run `sf project generate` then `sf ui-bundle generate --name ` inside * the project. Returns the absolute path to the generated project root. */ export function createProjectWithWebapp(session: TestSession, projectName: string, webAppName: string): string { const projectDir = createProject(session, projectName); - execSync(`sf webapp generate --name ${webAppName}`, { + execSync(`sf ui-bundle generate --name ${webAppName}`, { cwd: projectDir, stdio: 'pipe', + env: { ...process.env, HOME: REAL_HOME, USERPROFILE: REAL_HOME }, }); return projectDir; } /** - * Create a project with multiple webapps. Used to test selection flows when - * more than one webapp exists in a single SFDX project. + * Create a project with multiple UI bundles. Used to test selection flows when + * more than one UI bundle exists in a single SFDX project. */ export function createProjectWithMultipleWebapps( session: TestSession, @@ -114,24 +122,25 @@ export function createProjectWithMultipleWebapps( ): string { const projectDir = createProject(session, projectName); for (const name of webAppNames) { - execSync(`sf webapp generate --name ${name}`, { + execSync(`sf ui-bundle generate --name ${name}`, { cwd: projectDir, stdio: 'pipe', + env: { ...process.env, HOME: REAL_HOME, USERPROFILE: REAL_HOME }, }); } return projectDir; } /** - * Create the `webapplications/` directory (empty β€” no webapps inside). - * Used to test "empty webapplications folder" scenario. + * Create the `uiBundles/` directory (empty β€” no UI bundles inside). + * Used to test "empty uiBundles folder" scenario. */ export function createEmptyWebappsDir(projectDir: string): void { mkdirSync(webappPath(projectDir), { recursive: true }); } /** - * Create a webapp directory without the required `.webapplication-meta.xml`. + * Create a UI bundle directory without the required `.webapplication-meta.xml`. * Used to test "no metadata file" scenario. */ export function createWebappDirWithoutMeta(projectDir: string, name: string): void { @@ -139,14 +148,14 @@ export function createWebappDirWithoutMeta(projectDir: string, name: string): vo } /** - * Overwrite the `webapplication.json` manifest for a given webapp. + * Overwrite the `webapplication.json` manifest for a given UI bundle. */ export function writeManifest(projectDir: string, webAppName: string, manifest: Record): void { writeFileSync(join(webappPath(projectDir, webAppName), 'webapplication.json'), JSON.stringify(manifest, null, 2)); } /** - * Write a tiny Node.js HTTP server script into the webapp directory. + * Write a tiny Node.js HTTP server script into the UI bundle directory. * Returns the command string suitable for `dev.command` in the manifest. * * The script is CommonJS (.cjs) to avoid ESM/shell quoting issues. @@ -167,7 +176,7 @@ export function createDevServerScript(webappDir: string, port: number): string { } /** - * Convenience: create a project with a webapp whose manifest includes a + * Convenience: create a project with a UI bundle whose manifest includes a * `dev.command` that starts a tiny HTTP server on `devPort`, and * `dev.url` pointing to that port. Optionally sets `dev.port` (proxy port). * diff --git a/test/config/webappDiscovery.test.ts b/test/config/webappDiscovery.test.ts index bc9d0a0..f429192 100644 --- a/test/config/webappDiscovery.test.ts +++ b/test/config/webappDiscovery.test.ts @@ -18,13 +18,13 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { expect } from 'chai'; import { SfError, SfProject } from '@salesforce/core'; -import { DEFAULT_DEV_COMMAND, discoverWebapp } from '../../src/config/webappDiscovery.js'; +import { DEFAULT_DEV_COMMAND, discoverWebapp, UI_BUNDLES_FOLDER } from '../../src/config/webappDiscovery.js'; describe('webappDiscovery', () => { const testDir = join(process.cwd(), '.test-webapp-discovery'); - // Standard SFDX webapplications path - const sfdxWebappsPath = join(testDir, 'force-app', 'main', 'default', 'webapplications'); + // Standard SFDX uiBundles path + const sfdxWebappsPath = join(testDir, 'force-app', 'main', 'default', UI_BUNDLES_FOLDER); // Store original resolveProjectPath let originalResolveProjectPath: typeof SfProject.resolveProjectPath; @@ -120,7 +120,7 @@ describe('webappDiscovery', () => { } catch (error) { expect(error).to.be.instanceOf(SfError); expect((error as SfError).name).to.equal('WebappNotFoundError'); - expect((error as SfError).message).to.include('No webapplications folder found in the SFDX project'); + expect((error as SfError).message).to.include('No uiBundles folder found in the SFDX project'); } }); @@ -167,7 +167,7 @@ describe('webappDiscovery', () => { }); it('should auto-select webapp when inside its folder', async () => { - const webappsPath = join(testDir, 'webapplications'); + const webappsPath = join(testDir, UI_BUNDLES_FOLDER); mkdirSync(webappsPath, { recursive: true }); const myAppPath = createWebapp(webappsPath, 'my-app'); createWebapp(webappsPath, 'other-app'); @@ -179,7 +179,7 @@ describe('webappDiscovery', () => { }); it('should auto-select webapp when inside subfolder', async () => { - const webappsPath = join(testDir, 'webapplications'); + const webappsPath = join(testDir, UI_BUNDLES_FOLDER); mkdirSync(webappsPath, { recursive: true }); const myAppPath = createWebapp(webappsPath, 'my-app'); const srcPath = join(myAppPath, 'src'); @@ -193,7 +193,7 @@ describe('webappDiscovery', () => { }); it('should use meta.xml name (manifest.name is not used)', async () => { - const webappsPath = join(testDir, 'webapplications'); + const webappsPath = join(testDir, UI_BUNDLES_FOLDER); mkdirSync(webappsPath, { recursive: true }); const myAppPath = createWebapp(webappsPath, 'folder-name', { name: 'ManifestName' }); createWebapp(webappsPath, 'other-app'); @@ -230,7 +230,7 @@ describe('webappDiscovery', () => { }); it('should throw error when --name conflicts with current webapp directory', async () => { - const webappsPath = join(testDir, 'webapplications'); + const webappsPath = join(testDir, UI_BUNDLES_FOLDER); mkdirSync(webappsPath, { recursive: true }); const currentAppPath = createWebapp(webappsPath, 'current-app'); createWebapp(webappsPath, 'other-app'); @@ -248,7 +248,7 @@ describe('webappDiscovery', () => { }); it('should allow --name matching current webapp directory', async () => { - const webappsPath = join(testDir, 'webapplications'); + const webappsPath = join(testDir, UI_BUNDLES_FOLDER); mkdirSync(webappsPath, { recursive: true }); const currentAppPath = createWebapp(webappsPath, 'current-app'); createWebapp(webappsPath, 'other-app'); @@ -296,7 +296,7 @@ describe('webappDiscovery', () => { it('should discover webapps from multiple package directories', async () => { // Create project with two packages: force-app and packages/einstein - const einsteinWebappsPath = join(testDir, 'packages', 'einstein', 'main', 'default', 'webapplications'); + const einsteinWebappsPath = join(testDir, 'packages', 'einstein', 'main', 'default', UI_BUNDLES_FOLDER); mkdirSync(einsteinWebappsPath, { recursive: true }); setupSfdxProject([ { path: 'force-app', default: true }, From e3c9bf57a46e5bb4598588be8896ede33f5048ca Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 11:35:50 +0530 Subject: [PATCH 02/16] refactor: rename webapp references to ui-bundle and update package imports - Rename dev command, messages, schemas, and configs from webapp to ui-bundle - Update imports from @salesforce/webapp-experimental to @salesforce/ui-bundle - Remove ErrorPageRenderer (template inlined upstream) - Update all test helpers and NUT references Made-with: Cursor --- .env.template | 2 +- COMMANDS.md | 40 +- README.md | 28 +- SF_WEBAPP_DEV_GUIDE.md | 652 ++++++++++++++++++ messages/ui-bundle.dev.md | 54 +- package.json | 2 +- schemas/ui__bundle-dev.json | 11 +- src/commands/ui-bundle/dev.ts | 96 +-- src/config/ManifestWatcher.ts | 40 +- src/config/manifest.ts | 16 +- src/config/types.ts | 4 +- src/config/webappDiscovery.ts | 357 +++++----- src/proxy/ProxyServer.ts | 99 +-- src/server/DevServerManager.ts | 2 +- src/templates/ErrorPageRenderer.ts | 164 ----- test/commands/ui-bundle/dev.nut.ts | 100 +-- test/commands/ui-bundle/dev.test.ts | 48 +- test/commands/ui-bundle/devPort.nut.ts | 16 +- test/commands/ui-bundle/devWithUrl.nut.ts | 50 +- .../ui-bundle/helpers/devServerUtils.ts | 13 +- .../ui-bundle/helpers/webappProjectUtils.ts | 52 +- test/config/ManifestWatcher.test.ts | 16 +- test/config/types.test.ts | 14 +- test/config/webappDiscovery.test.ts | 228 +++--- test/templates/ErrorPageRenderer.test.ts | 75 -- yarn.lock | 81 +-- 26 files changed, 1304 insertions(+), 956 deletions(-) create mode 100644 SF_WEBAPP_DEV_GUIDE.md delete mode 100644 src/templates/ErrorPageRenderer.ts delete mode 100644 test/templates/ErrorPageRenderer.test.ts diff --git a/.env.template b/.env.template index 9b42f3e..d80deb8 100644 --- a/.env.template +++ b/.env.template @@ -3,7 +3,7 @@ # # Option A: AUTH_URL (simplest β€” recommended for getting started) # 1. sf org login web --alias nut-org -# 2. sf org display -o nut-org --verbose --json | jq -r .result.sfdxAuthUrl +# 2. sf org display -o nut-org --json | jq -r .result.sfdxAuthUrl # 3. Paste the value below # TESTKIT_AUTH_URL= diff --git a/COMMANDS.md b/COMMANDS.md index 0ee6eb8..2dde89a 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -2,47 +2,43 @@ -- [`sf ui-bundle dev`](#sf-webui-dev) +- [`sf webapp dev`](#sf-webapp-dev) -## `sf ui-bundle dev` +## `sf webapp dev` -Start a local development proxy server for webui ui-bundle development with Salesforce authentication. +Preview a web app locally without needing to deploy ``` USAGE - $ sf ui-bundle dev -n -o [--json] [--flags-dir ] [-u ] [-p ] [--open] + $ sf webapp dev -n [--json] [--flags-dir ] [-t ] [-p ] -REQUIRED FLAGS - -n, --name= Name of the webapp (must match webapplication.json) - -o, --target-org= Salesforce org to authenticate against - -OPTIONAL FLAGS - -u, --url= Dev server URL. Command mode: override default 5173. URL-only: required (server must be running) - -p, --port= Proxy server port (default: 4545) - --open Open browser automatically +FLAGS + -n, --name= (required) Identifies the Web Application + -p, --port= [default: 5173] Port for the dev server + -t, --target= Selects which Web Application target to use for the preview (e.g., Lightning App, Site) GLOBAL FLAGS --flags-dir= Import flag values from a directory. --json Format output as json. DESCRIPTION - Starts a local HTTP proxy that injects Salesforce authentication and routes - requests between your dev server and Salesforce APIs. In command mode, - spawns and monitors the dev server (default URL: localhost:5173). In - URL-only mode, connects to an already-running dev server. + Preview a web app locally without needing to deploy + + Starts a local development server for a Web Application, using the local project files. This enables rapid + development with hot reloading and immediate feedback. EXAMPLES - Command mode (CLI starts dev server, default port 5173): + Start the development server: - $ sf ui-bundle dev --name myapp --target-org myorg --open + $ sf webapp dev --name myWebApp - URL-only mode (dev server already running): + Start the development server with a specific target: - $ sf ui-bundle dev --name myapp --target-org myorg --url http://localhost:5173 --open + $ sf webapp dev --name myWebApp --target "LightningApp" - Custom proxy port: + Start the development server on a custom port: - $ sf ui-bundle dev --name myapp --target-org myorg --port 8080 --open + $ sf webapp dev --name myWebApp --port 8080 ``` diff --git a/README.md b/README.md index 59bcdde..0c2d284 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# plugin-ui-bundle-dev +# plugin-app-dev -[![NPM](https://img.shields.io/npm/v/@salesforce/plugin-ui-bundle-dev.svg?label=@salesforce/plugin-ui-bundle-dev)](https://www.npmjs.com/package/@salesforce/plugin-ui-bundle-dev) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-ui-bundle-dev.svg)](https://npmjs.org/package/@salesforce/plugin-ui-bundle-dev) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) +[![NPM](https://img.shields.io/npm/v/@salesforce/plugin-app-dev.svg?label=@salesforce/plugin-app-dev)](https://www.npmjs.com/package/@salesforce/plugin-app-dev) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-app-dev.svg)](https://npmjs.org/package/@salesforce/plugin-app-dev) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) # Salesforce CLI App Dev Plugin @@ -24,7 +24,7 @@ We always recommend using the latest version of these commands bundled with the 1. **Install the plugin:** ```bash - sf plugins install @salesforce/plugin-ui-bundle-dev + sf plugins install @salesforce/plugin-app-dev ``` 2. **Authenticate with Salesforce:** @@ -50,12 +50,12 @@ We always recommend using the latest version of these commands bundled with the 4. **Start development:** ```bash - sf ui-bundle dev --name myapp --target-org myorg --open + sf webapp dev --name myapp --target-org myorg --open ``` ## Documentation -πŸ“š **[Complete Guide](SF_UI_BUNDLE_DEV_GUIDE.md)** - Comprehensive documentation covering: +πŸ“š **[Complete Guide](SF_WEBAPP_DEV_GUIDE.md)** - Comprehensive documentation covering: - Overview and architecture - Getting started (5-minute quick start) @@ -69,7 +69,7 @@ We always recommend using the latest version of these commands bundled with the ## Install ```bash -sf plugins install @salesforce/plugin-ui-bundle-dev@x.y.z +sf plugins install @salesforce/plugin-app-dev@x.y.z ``` ## Issues @@ -101,7 +101,7 @@ To build the plugin locally, make sure to have yarn installed and run the follow ```bash # Clone the repository -git clone git@github.com:salesforcecli/plugin-ui-bundle-dev +git clone git@github.com:salesforcecli/plugin-app-dev # Install the dependencies and compile yarn && yarn build @@ -125,9 +125,9 @@ sf plugins ## Commands -### `sf ui-bundle dev` +### `sf webapp dev` -Start a local development proxy server for ui-bundle development with Salesforce authentication. +Start a local development proxy server for webapp development with Salesforce authentication. **Two operating modes:** @@ -136,7 +136,7 @@ Start a local development proxy server for ui-bundle development with Salesforce ```bash USAGE - $ sf ui-bundle dev --name --target-org [options] + $ sf webapp dev --name --target-org [options] REQUIRED FLAGS -n, --name= Name of the webapp (must match webapplication.json) @@ -156,18 +156,18 @@ DESCRIPTION EXAMPLES Command mode (CLI starts dev server, default port 5173): - $ sf ui-bundle dev --name myapp --target-org myorg --open + $ sf webapp dev --name myapp --target-org myorg --open URL-only mode (dev server already running): - $ sf ui-bundle dev --name myapp --target-org myorg --url http://localhost:5173 --open + $ sf webapp dev --name myapp --target-org myorg --url http://localhost:5173 --open Custom proxy port: - $ sf ui-bundle dev --name myapp --target-org myorg --port 8080 --open + $ sf webapp dev --name myapp --target-org myorg --port 8080 --open SEE ALSO - - Complete Guide: SF_UI_BUNDLE_DEV_GUIDE.md + - Complete Guide: SF_WEBAPP_DEV_GUIDE.md ``` diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_WEBAPP_DEV_GUIDE.md new file mode 100644 index 0000000..98c4e23 --- /dev/null +++ b/SF_WEBAPP_DEV_GUIDE.md @@ -0,0 +1,652 @@ +# Salesforce Webapp Dev Command Guide + +> **Develop web applications with seamless Salesforce integration** + +--- + +## Overview + +The `sf webapp dev` command enables local development of modern web applications (React, Vue, Angular, etc.) with automatic Salesforce authentication. It intelligently discovers your webapp configuration, handles proxy routing, injects authentication headers, and supports hot reload - so you can focus on building your app. + +### Key Features + +- **Auto-Discovery**: Automatically finds webapps in `webapplications/` folder +- **Optional Manifest**: `webapplication.json` is optional - uses sensible defaults +- **Auto-Selection**: Automatically selects webapp when running from inside its folder +- **Interactive Selection**: Prompts with arrow-key navigation when multiple webapps 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 + +--- + +## Quick Start + +### 1. Create your webapp in the SFDX project structure + +``` +my-sfdx-project/ +β”œβ”€β”€ sfdx-project.json +└── force-app/main/default/webapplications/ + └── my-app/ + β”œβ”€β”€ my-app.webapplication-meta.xml + β”œβ”€β”€ package.json + β”œβ”€β”€ src/ + └── webapplication.json +``` + +### 2. Run the command + +```bash +sf webapp dev --target-org myOrg --open +``` + +### 3. Start developing + +Browser opens to `http://localhost:4545` with your app running and Salesforce authentication ready. + +> **Note**: +> +> - `{name}.webapplication-meta.xml` is **required** to identify a valid webapp +> - `webapplication.json` is 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 + +--- + +## Command Syntax + +```bash +sf webapp dev [OPTIONS] +``` + +### Options + +| Option | Short | Description | Default | +| -------------- | ----- | ---------------------------------- | ------------- | +| `--target-org` | `-o` | Salesforce org alias or username | Required | +| `--name` | `-n` | Web application 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 | + +### Examples + +```bash +# Simplest - auto-discovers webapplication.json +sf webapp dev --target-org myOrg + +# With browser auto-open +sf webapp dev --target-org myOrg --open + +# Specify webapp by name (when multiple exist) +sf webapp dev --name myApp --target-org myOrg + +# Custom port +sf webapp dev --target-org myOrg --port 8080 + +# Explicit dev server URL (skip auto-detection) +sf webapp dev --target-org myOrg --url http://localhost:5173 + +# Debug mode +SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg +``` + +--- + +## Webapp Discovery + +The command discovers webapps using a simplified, deterministic algorithm. Webapps are identified by the presence of a `{name}.webapplication-meta.xml` file (SFDX metadata format). The optional `webapplication.json` file provides dev configuration. + +### How Discovery Works + +```mermaid +flowchart TD + Start["sf webapp dev"] --> CheckInside{"Inside webapplications/
webapp folder?"} + + CheckInside -->|Yes| HasNameInside{"--name provided?"} + HasNameInside -->|Yes, different| ErrorConflict["Error: --name conflicts
with current directory"] + HasNameInside -->|No or same| AutoSelect["Auto-select current webapp"] + + CheckInside -->|No| CheckSFDX{"In SFDX project?
(sfdx-project.json)"} + + CheckSFDX -->|Yes| CheckPath["Check force-app/main/
default/webapplications/"] + CheckPath --> HasName{"--name provided?"} + + CheckSFDX -->|No| CheckMetaXml{"Current dir has
.webapplication-meta.xml?"} + CheckMetaXml -->|Yes| UseStandalone["Use current dir as webapp"] + CheckMetaXml -->|No| ErrorNone["Error: No webapp found"] + + HasName -->|Yes| SearchByName["Find webapp by name"] + HasName -->|No| Prompt["Interactive selection prompt
(always, even if 1 webapp)"] + + SearchByName --> UseWebapp["Use webapp"] + AutoSelect --> UseWebapp + UseStandalone --> UseWebapp + Prompt --> UseWebapp + + UseWebapp --> StartDev["Start dev server and proxy"] +``` + +### Discovery Behavior + +| Scenario | Behavior | +| ----------------------------------- | --------------------------------------------------------- | +| `--name myApp` provided | Finds webapp by name, starts dev server | +| Running from inside webapp folder | Auto-selects that webapp | +| `--name` conflicts with current dir | Error: must match current webapp or run from project root | +| At SFDX project root | **Always prompts** for webapp selection | +| Outside SFDX project with meta.xml | Uses current directory as standalone webapp | +| No webapp found | Shows error with helpful message | + +### Folder Structure (SFDX Project) + +``` +my-sfdx-project/ +β”œβ”€β”€ sfdx-project.json # SFDX project marker +└── force-app/main/default/ + └── webapplications/ # Standard SFDX location + β”œβ”€β”€ app-one/ # Webapp 1 (with dev config) + β”‚ β”œβ”€β”€ app-one.webapplication-meta.xml # Required: identifies as webapp + β”‚ β”œβ”€β”€ webapplication.json # Optional: dev configuration + β”‚ β”œβ”€β”€ package.json + β”‚ └── src/ + └── app-two/ # Webapp 2 (no dev config) + β”œβ”€β”€ app-two.webapplication-meta.xml # Required + β”œβ”€β”€ package.json + └── src/ +``` + +### Discovery Strategy + +The command uses a simplified, deterministic approach: + +1. **Inside webapp folder**: If running from `webapplications//` or deeper, auto-selects that webapp +2. **SFDX project root**: Uses fixed path `force-app/main/default/webapplications/` +3. **Standalone**: If current directory has a `.webapplication-meta.xml` file, uses it directly + +**Important**: Only directories containing a `{name}.webapplication-meta.xml` file are recognized as valid webapps. + +### Interactive Selection + +When multiple webapps are found, you'll see an interactive prompt: + +``` +Found 3 webapps in project +? Select the webapp to run: (Use arrow keys) +❯ app-one (webapplications/app-one) + app-two (webapplications/app-two) [no manifest] + app-three (webapplications/app-three) +``` + +Format: + +- **With manifest**: `folder-name (path)` +- **No manifest**: `folder-name (path) [no manifest]` + +--- + +## Architecture + +### Request Flow + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 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 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### How Requests Are Handled + +**Static assets (JS, CSS, HTML, images):** + +``` +Browser β†’ Proxy β†’ Dev Server β†’ Response +``` + +**Salesforce API calls (`/services/*`):** + +``` +Browser β†’ Proxy β†’ [Auth Headers Injected] β†’ Salesforce β†’ Response +``` + +--- + +## Configuration + +### Dev Server URL Resolution + +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) + +### webapplication.json Schema + +The `webapplication.json` file is **optional**. All fields are also optional - missing fields use defaults. + +#### Dev Configuration + +| 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):** + +```json +{ + "dev": { + "command": "npm run dev" + } +} +``` + +- CLI runs `npm run dev` and 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):** + +```json +{ + "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 dev` in 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 Configuration (Optional) + +```json +{ + "routing": { + "rewrites": [{ "route": "/api/:path*", "target": "/services/apexrest/:path*" }], + "redirects": [{ "route": "/old-path", "target": "/new-path", "statusCode": 301 }], + "trailingSlash": "never", + "fallback": "/index.html" + } +} +``` + +### Example: Minimal (No Manifest) + +``` +webapplications/ +└── my-dashboard/ + β”œβ”€β”€ package.json # Has "scripts": { "dev": "vite" } + └── src/ +``` + +Run: `sf webapp dev --target-org myOrg` + +Console output: + +``` +Warning: No webapplication.json found for webapp "my-dashboard" + Location: my-dashboard + Using defaults: + β†’ Name: "my-dashboard" (derived from folder) + β†’ Command: "npm run dev" + β†’ Manifest watching: disabled + πŸ’‘ To customize, create a webapplication.json file in your webapp directory. + +βœ… Using webapp: my-dashboard (webapplications/my-dashboard) + +βœ… Ready for development! + β†’ Proxy: http://localhost:4545 (open this in your browser) + β†’ Dev server: http://localhost:5173 +Press Ctrl+C to stop +``` + +### Example: Dev + Routing + +```json +{ + "dev": { + "command": "npm run dev" + }, + "routing": { + "rewrites": [{ "route": "/api/:path*", "target": "/services/apexrest/:path*" }], + "trailingSlash": "never" + } +} +``` + +--- + +## Features + +### Manifest Hot Reload + +Edit `webapplication.json` while running - changes apply automatically: + +```bash +# Console output when you change webapplication.json: +Manifest changed detected +βœ“ Manifest reloaded successfully +Dev server URL updated to: http://localhost:5174 +``` + +> **Note**: Manifest watching is only enabled when `webapplication.json` exists. Webapps without manifests don't have this feature. + +### Health Monitoring + +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 + +### WebSocket Support + +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. + +### Code Builder Support + +Automatically detects Salesforce Code Builder environment and binds to `0.0.0.0` for proper port forwarding in cloud environments. + +--- + +## The `--url` Flag + +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 | + +### Connect to Existing Dev Server (Proxy-Only Mode) + +When you run the dev server yourself: + +```bash +# Terminal 1: Start your dev server manually +cd my-webapp +npm run dev +# Output: Local: http://localhost:5173/ + +# Terminal 2: Connect proxy to your running server +sf webapp dev --url http://localhost:5173 --target-org myOrg +``` + +**Output:** + +``` +βœ… 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) +``` + +### Override Default Port (Command Mode) + +When using `dev.command`, the default URL is `http://localhost:5173`. Override with `--url` if your dev server uses a different port: + +```bash +sf webapp dev --url http://localhost:3000 --target-org myOrg +``` + +If the URL is not reachable, the CLI starts the dev server and uses the actual URL (with a warning if it differs). + +--- + +## Troubleshooting + +### "No webapp found" or "No valid webapps" + +Ensure your webapp has the required `.webapplication-meta.xml` file: + +``` +force-app/main/default/webapplications/ +└── my-app/ + β”œβ”€β”€ my-app.webapplication-meta.xml # Required! + β”œβ”€β”€ package.json + └── webapplication.json # Optional (for dev config) +``` + +The `.webapplication-meta.xml` file identifies a valid SFDX webapp. Without it, the directory is ignored. + +### "You are inside webapp X but specified --name Y" + +This error occurs when you're inside one webapp folder but try to run a different webapp: + +```bash +# You're in FirstWebApp folder but trying to run SecondWebApp +cd webapplications/FirstWebApp +sf webapp dev --name SecondWebApp --target-org myOrg # Error! +``` + +**Solutions:** + +- Remove `--name` to use the current webapp +- Navigate to the project root and use `--name` +- Navigate to the correct webapp folder + +### "No webapp found with name X" + +The `--name` flag matches the folder name of the webapp. + +```bash +# This looks for webapp named "myApp" +sf webapp dev --name myApp --target-org myOrg +``` + +### "Dependencies Not Installed" / "command not found" + +Install dependencies in your webapp folder: + +```bash +cd webapplications/my-app +npm install +``` + +### "No Dev Server Detected" + +1. Ensure dev server is running: `npm run dev` +2. Verify URL in `webapplication.json` is correct +3. Try explicit URL: `sf webapp dev --url http://localhost:5173 --target-org myOrg` + +### "Port 4545 already in use" + +```bash +# Use a different port +sf webapp dev --port 8080 --target-org myOrg + +# Or find and kill the process using the port +lsof -i :4545 +kill -9 +``` + +### "Authentication Failed" + +Re-authorize your Salesforce org: + +```bash +sf org login web --alias myOrg +``` + +### Debug Mode + +Enable 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** + +```bash +# Tail today's log file, filtering for webapp messages +tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev + +# Or for cleaner output (requires jq): +tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev | jq -r '.msg' +``` + +**Step 2: Run command in Terminal 2** + +```bash +SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg +``` + +**Example debug output:** + +``` +Discovering webapplication.json manifest(s)... +Using webapp: myApp at webapplications/my-app +Manifest loaded: myApp +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 +``` + +--- + +## VSCode Integration + +The command integrates with the Salesforce VSCode UI Preview extension (`salesforcedx-vscode-ui-preview`): + +1. Extension detects `webapplication.json` in workspace +2. User clicks "Preview" button on the file +3. Extension executes: `sf webapp dev --target-org --open` +4. If multiple webapps exist, uses `--name` to specify which one +5. Browser opens with the app running + +--- + +## JSON Output + +For scripting and CI/CD, use the `--json` flag: + +```bash +sf webapp dev --target-org myOrg --json +``` + +Output: + +```json +{ + "status": 0, + "result": { + "url": "http://localhost:4545", + "devServerUrl": "http://localhost:5173" + } +} +``` + +--- + +## Plugin Development + +### Building the Plugin + +```bash +cd /path/to/plugin-app-dev + +# Install dependencies +yarn install + +# Build +yarn build + +# Link to SF CLI +sf plugins link . + +# Verify installation +sf plugins +``` + +### After Code Changes + +```bash +yarn build # Rebuild - no re-linking needed +``` + +### Project Structure + +``` +plugin-app-dev/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ commands/webapp/ +β”‚ β”‚ └── dev.ts # Main command implementation +β”‚ β”œβ”€β”€ auth/ +β”‚ β”‚ └── org.ts # Salesforce authentication +β”‚ β”œβ”€β”€ 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 +β”‚ β”œβ”€β”€ error/ +β”‚ β”‚ β”œβ”€β”€ ErrorHandler.ts # Error creation utilities +β”‚ β”‚ β”œβ”€β”€ DevServerErrorParser.ts +β”‚ β”‚ └── ErrorPageRenderer.ts +β”‚ └── templates/ +β”‚ └── error-page.html # Error page template +β”œβ”€β”€ messages/ +β”‚ └── webapp.dev.md # CLI messages and help text +└── schemas/ + └── webapp-dev.json # JSON schema for output +``` + +### Key Components + +| Component | Purpose | +| ---------------------- | ------------------------------------------------ | +| `dev.ts` | Command orchestration and lifecycle | +| `webappDiscovery.ts` | SFDX project detection and webapp discovery | +| `org.ts` | Salesforce authentication token management | +| `ProxyServer.ts` | HTTP proxy with WebSocket support | +| `handler.ts` | Request routing to dev server or Salesforce | +| `DevServerManager.ts` | Dev server process spawning and monitoring | +| `ManifestWatcher.ts` | webapplication.json file watching for hot reload | +| `ErrorPageRenderer.ts` | Browser error page generation | + +--- + +**Repository:** [github.com/salesforcecli/plugin-app-dev](https://github.com/salesforcecli/plugin-app-dev) diff --git a/messages/ui-bundle.dev.md b/messages/ui-bundle.dev.md index 83f2cae..5e49962 100644 --- a/messages/ui-bundle.dev.md +++ b/messages/ui-bundle.dev.md @@ -1,26 +1,26 @@ # summary -Preview a web application locally and in real-time, without deploying it to your org. +Preview a UI bundle locally and in real-time, without deploying it to your org. # description -This command starts a local development (dev) server so you can preview a web application using the local metadata files in your DX project. Using a local preview helps you quickly develop web applications, because you don't have to continually deploy metadata to your org. +This command starts a local development (dev) server so you can preview a UI bundle using the local metadata files in your DX project. Using a local preview helps you quickly develop UI bundles, because you don't have to continually deploy metadata to your org. -The command also launches a local proxy server that sits between your web application and Salesforce, automatically injecting authentication headers from Salesforce CLI's stored tokens. The proxy allows your web app to make authenticated API calls to Salesforce without exposing credentials. +The command also launches a local proxy server that sits between your UI bundle and Salesforce, automatically injecting authentication headers from Salesforce CLI's stored tokens. The proxy allows your UI bundle to make authenticated API calls to Salesforce without exposing credentials. -Even though you're previewing the web application locally and not deploying anything to an org, you're still required to authorize and specify an org to use this command. +Even though you're previewing the UI bundle locally and not deploying anything to an org, you're still required to authorize and specify an org to use this command. -Salesforce web applications are represented by the WebApplication metadata type. +Salesforce UI bundles are represented by the UiBundle metadata type. # flags.name.summary -Name of the web application to preview. +Name of the UI bundle to preview. # flags.name.description -The unique name of the web application, as defined by the "name" property in the webapplication.json runtime configuration file. The webapplication.json file is located in the "uiBundles" metadata directory of your DX project, such as force-app/main/default/uiBundles/MyApp/webapplication.json. +The unique name of the UI bundle, as defined by the "name" property in the ui-bundle.json runtime configuration file. The ui-bundle.json file is located in the "uiBundles" metadata directory of your DX project, such as force-app/main/default/uiBundles/MyApp/ui-bundle.json. -If you don't specify this flag, the command automatically discovers the webapplication.json files in the current directory and subdirectories. If the command finds only one webapplication.json, it automatically uses it. If it finds multiple files, the command prompts you to select one. +If you don't specify this flag, the command automatically discovers the ui-bundle.json files in the current directory and subdirectories. If the command finds only one ui-bundle.json, it automatically uses it. If it finds multiple files, the command prompts you to select one. # flags.url.summary @@ -28,9 +28,9 @@ URL where your developer server runs, such as https://localhost:5173. All UI, st # flags.url.description -You must specify this flag if the web application's webapplication.json file doesn't contain a value for either the "dev.command" or "dev.url" configuration properties. All non-Salesforce API requests are forwarded to this URL. +You must specify this flag if the UI bundle's ui-bundle.json file doesn't contain a value for either the "dev.command" or "dev.url" configuration properties. All non-Salesforce API requests are forwarded to this URL. -If you specify this flag, it overrides the value in the webapplication.json file. +If you specify this flag, it overrides the value in the ui-bundle.json file. This is the order of precedence that the dev server uses for the URL: --url flag > manifest dev.url > URL from the dev server process (which was started using either manifest dev.command or default npm run dev). @@ -52,17 +52,17 @@ This flag saves you from manually copying and pasting the URL. The browser opens # examples -- Start the local development (dev) server by automatically discovering the web application's webapplication.json file; use the org with alias "myorg": +- Start the local development (dev) server by automatically discovering the UI bundle's ui-bundle.json file; use the org with alias "myorg": <%= config.bin %> <%= command.id %> --target-org myorg -- Start the dev server by explicitly specifying the web application's name: +- Start the dev server by explicitly specifying the UI bundle's name: - <%= config.bin %> <%= command.id %> --name myWebApp --target-org myorg + <%= config.bin %> <%= command.id %> --name myUiBundle --target-org myorg - Start at the specified dev server URL: - <%= config.bin %> <%= command.id %> --name myWebApp --url http://localhost:5173 --target-org myorg + <%= config.bin %> <%= command.id %> --name myUiBundle --url http://localhost:5173 --target-org myorg - Start with a custom proxy port and automatically open the proxy server URL in your browser: @@ -182,7 +182,7 @@ Failed to watch manifest: %s. # error.dev-url-unreachable Dev server unreachable at %s. -Start your dev server manually at that URL, or add dev.command to webapplication.json to start it automatically. +Start your dev server manually at that URL, or add dev.command to ui-bundle.json to start it automatically. # error.dev-url-unreachable-with-flag @@ -197,35 +197,35 @@ Port %s is already in use. Try specifying a different port with the --port flag %s -# info.multiple-webapps-found +# info.multiple-uiBundles-found -Found %s webapps in project. +Found %s UI bundles in project. -# info.webapp-auto-selected +# info.uiBundle-auto-selected -Auto-selected webapp "%s" (running from inside its folder). +Auto-selected UI bundle "%s" (running from inside its folder). -# info.using-webapp +# info.using-uiBundle -βœ… Using webapp: %s (%s). +βœ… Using UI bundle: %s (%s). -# info.starting-webapp +# info.starting-uiBundle βœ… Starting %s. -# prompt.select-webapp +# prompt.select-uiBundle -Select the webapp to run: +Select the UI bundle to run: # info.no-manifest-defaults -No webapplication.json found. Using defaults: dev command=%s, proxy port=%s. +No ui-bundle.json found. Using defaults: dev command=%s, proxy port=%s. Tip: See "sf ui-bundle dev --help" for configuration options. # warning.empty-manifest -No dev configuration in webapplication.json - using defaults (command: %s). +No dev configuration in ui-bundle.json - using defaults (command: %s). Tip: See "sf ui-bundle dev --help" for configuration options. @@ -244,4 +244,4 @@ The proxy will use the actual dev server URL. # info.vite-proxy-detected -Vite WebApp proxy detected at %s - using Vite's built-in proxy (standalone proxy skipped). +Vite UI bundle proxy detected at %s - using Vite's built-in proxy (standalone proxy skipped). diff --git a/package.json b/package.json index f4e1caa..de796db 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@salesforce/core": "^8.25.1", "@salesforce/kit": "^3.2.4", "@salesforce/sf-plugins-core": "^12.2.6", - "@salesforce/webapp-experimental": "^1.23.0", + "@salesforce/ui-bundle": "file:../webapps/packages/webapps", "chokidar": "^3.6.0", "http-proxy": "^1.18.1", "micromatch": "^4.0.8", diff --git a/schemas/ui__bundle-dev.json b/schemas/ui__bundle-dev.json index f55d1e0..1655f0c 100644 --- a/schemas/ui__bundle-dev.json +++ b/schemas/ui__bundle-dev.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/WebAppDevResult", + "$ref": "#/definitions/UiBundleDevResult", "definitions": { - "WebAppDevResult": { + "UiBundleDevResult": { "type": "object", "properties": { "url": { @@ -14,12 +14,9 @@ "description": "Dev server URL being proxied" } }, - "required": [ - "url", - "devServerUrl" - ], + "required": ["url", "devServerUrl"], "additionalProperties": false, "description": "Command execution result What the sf ui-bundle dev command returns to the user" } } -} \ No newline at end of file +} diff --git a/src/commands/ui-bundle/dev.ts b/src/commands/ui-bundle/dev.ts index 41ee789..3a97d2c 100644 --- a/src/commands/ui-bundle/dev.ts +++ b/src/commands/ui-bundle/dev.ts @@ -18,17 +18,17 @@ import open from 'open'; import select from '@inquirer/select'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Logger, Messages, SfError } from '@salesforce/core'; -import type { WebAppDevResult, DevServerError } from '../../config/types.js'; -import type { WebAppManifest } from '../../config/manifest.js'; +import type { UiBundleDevResult, DevServerError } from '../../config/types.js'; +import type { UiBundleManifest } from '../../config/manifest.js'; import { ManifestWatcher } from '../../config/ManifestWatcher.js'; import { DevServerManager } from '../../server/DevServerManager.js'; import { ProxyServer } from '../../proxy/ProxyServer.js'; -import { discoverWebapp, DEFAULT_DEV_COMMAND, type DiscoveredWebapp } from '../../config/webappDiscovery.js'; +import { discoverUiBundle, DEFAULT_DEV_COMMAND, type DiscoveredUiBundle } from '../../config/webappDiscovery.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-ui-bundle-dev', 'ui-bundle.dev'); -export default class UiBundleDev extends SfCommand { +export default class UiBundleDev extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); @@ -74,30 +74,30 @@ export default class UiBundleDev extends SfCommand { } /** - * Prompt user to select a webapp from multiple discovered webapps + * Prompt user to select a uiBundle from multiple discovered uiBundles * Uses interactive arrow-key selection (standard SF CLI pattern) */ - private static async promptUiBundleSelection(webapps: DiscoveredWebapp[]): Promise { + private static async promptUiBundleSelection(uiBundles: DiscoveredUiBundle[]): Promise { const WARNING = '\u26A0\uFE0F'; // ⚠️ - const choices = webapps.map((webapp) => { - if (webapp.hasManifest) { + const choices = uiBundles.map((uiBundle) => { + if (uiBundle.hasManifest) { // Has manifest - show name only return { - name: webapp.name, - value: webapp, + name: uiBundle.name, + value: uiBundle, }; } else { // No manifest - show warning symbol return { - name: `${webapp.name} - ${WARNING} No Manifest`, - value: webapp, + name: `${uiBundle.name} - ${WARNING} No Manifest`, + value: uiBundle, }; } }); return select({ - message: messages.getMessage('prompt.select-webapp'), + message: messages.getMessage('prompt.select-uiBundle'), choices, }); } @@ -133,7 +133,7 @@ export default class UiBundleDev extends SfCommand { intervalMs = 500, start = Date.now() ): Promise { - if (await UiBundleDev.isUrlReachable(url)) { + if (await UiBundleDev.isUrlReachable(url)) { return true; } if (Date.now() - start >= timeoutMs) { @@ -144,7 +144,7 @@ export default class UiBundleDev extends SfCommand { } /** - * Check if Vite's WebAppProxyHandler is active at the dev server URL. + * Check if Vite's UiBundleProxyHandler is active at the dev server URL. * The Vite plugin responds to a health check query parameter with a custom header * when the proxy middleware is active. * @@ -160,7 +160,7 @@ export default class UiBundleDev extends SfCommand { method: 'GET', signal: AbortSignal.timeout(3000), // 3 second timeout }); - return response.headers.get('X-Salesforce-WebApp-Proxy') === 'true'; + return response.headers.get('X-Salesforce-UiBundle-Proxy') === 'true'; } catch { // Health check failed - Vite proxy not active return false; @@ -168,7 +168,7 @@ export default class UiBundleDev extends SfCommand { } // eslint-disable-next-line complexity - public async run(): Promise { + public async run(): Promise { const { flags } = await this.parse(UiBundleDev); // Initialize logger from @salesforce/core for debug logging @@ -176,41 +176,41 @@ export default class UiBundleDev extends SfCommand { this.logger = await Logger.child('UiBundleDev'); // Declare variables outside try block for catch block access - let manifest: WebAppManifest | null = null; + let manifest: UiBundleManifest | null = null; let devServerUrl: string | null = null; let orgUsername = ''; try { - // Step 1: Discover and select webapp - this.logger.debug('Discovering webapplication.json manifest(s)...'); + // Step 1: Discover and select uiBundle + this.logger.debug('Discovering ui-bundle.json manifest(s)...'); - const { webapp: discoveredWebapp, allWebapps, autoSelected } = await discoverWebapp(flags.name); + const { uiBundle: discoveredUiBundle, allUiBundles, autoSelected } = await discoverUiBundle(flags.name); - // Handle multiple webapps case - prompt user to select - let selectedWebapp: DiscoveredWebapp; - if (!discoveredWebapp) { - this.log(messages.getMessage('info.multiple-webapps-found', [String(allWebapps.length)])); + // Handle multiple uiBundles case - prompt user to select + let selectedUiBundle: DiscoveredUiBundle; + if (!discoveredUiBundle) { + this.log(messages.getMessage('info.multiple-uiBundles-found', [String(allUiBundles.length)])); - selectedWebapp = await UiBundleDev.promptUiBundleSelection(allWebapps); + selectedUiBundle = await UiBundleDev.promptUiBundleSelection(allUiBundles); } else { - selectedWebapp = discoveredWebapp; + selectedUiBundle = discoveredUiBundle; - // Show info message if webapp was auto-selected because user is inside its folder + // Show info message if uiBundle was auto-selected because user is inside its folder if (autoSelected) { - this.log(messages.getMessage('info.webapp-auto-selected', [selectedWebapp.name])); + this.log(messages.getMessage('info.uiBundle-auto-selected', [selectedUiBundle.name])); } } - // The webapp directory path (where the webapp lives) - const webappDir = selectedWebapp.path; + // The uiBundle directory path (where the uiBundle lives) + const uiBundleDir = selectedUiBundle.path; - this.logger.debug(`Using webapp: ${selectedWebapp.name} at ${selectedWebapp.relativePath}`); + this.logger.debug(`Using uiBundle: ${selectedUiBundle.name} at ${selectedUiBundle.relativePath}`); - // Step 2: Handle manifest-based vs no-manifest webapps - if (selectedWebapp.hasManifest && selectedWebapp.manifestPath) { - // Webapp has manifest - load and watch it + // Step 2: Handle manifest-based vs no-manifest uiBundles + if (selectedUiBundle.hasManifest && selectedUiBundle.manifestPath) { + // UI bundle has manifest - load and watch it this.manifestWatcher = new ManifestWatcher({ - manifestPath: selectedWebapp.manifestPath, + manifestPath: selectedUiBundle.manifestPath, watch: true, }); @@ -227,8 +227,8 @@ export default class UiBundleDev extends SfCommand { // Show starting message this.log(''); - this.log(messages.getMessage('info.starting-webapp', [selectedWebapp.name])); - this.logger.debug(`Manifest loaded: ${selectedWebapp.name}`); + this.log(messages.getMessage('info.starting-uiBundle', [selectedUiBundle.name])); + this.logger.debug(`Manifest loaded: ${selectedUiBundle.name}`); // Setup manifest change handler this.manifestWatcher.on('change', (event) => { @@ -266,18 +266,18 @@ export default class UiBundleDev extends SfCommand { const defaultPort = flags.port ?? 4545; this.log(messages.getMessage('info.no-manifest-defaults', [DEFAULT_DEV_COMMAND, String(defaultPort)])); this.log(''); - this.log(messages.getMessage('info.starting-webapp', [selectedWebapp.name])); + this.log(messages.getMessage('info.starting-uiBundle', [selectedUiBundle.name])); } // Step 3: Resolve dev server URL (config-driven, no stdout parsing) // Priority: --url > dev.url > (dev.command or no-manifest or no dev config ? default localhost:5173 : throw) // Use default URL when: no manifest, no dev section, no dev.command, or dev.command is non-empty const hasExplicitCommand = Boolean(manifest?.dev?.command?.trim()); - const hasDevCommand = !selectedWebapp.hasManifest || !manifest?.dev?.command || hasExplicitCommand; + const hasDevCommand = !selectedUiBundle.hasManifest || !manifest?.dev?.command || hasExplicitCommand; const resolvedUrl = flags.url ?? manifest?.dev?.url ?? (hasDevCommand ? 'http://localhost:5173' : null); if (!resolvedUrl) { throw new SfError( - '❌ Unable to determine dev server URL. Specify --url or configure dev.url or dev.command in webapplication.json.', + '❌ Unable to determine dev server URL. Specify --url or configure dev.url or dev.command in ui-bundle.json.', 'DevServerUrlError' ); } @@ -303,12 +303,12 @@ export default class UiBundleDev extends SfCommand { // dev.url in manifest but no dev.command - don't start (we can't control the port) throw new SfError(messages.getMessage('error.dev-url-unreachable', [resolvedUrl]), 'DevServerUrlError', [ `Ensure your dev server is running at ${resolvedUrl}`, - 'Or add dev.command to webapplication.json to start it automatically', + 'Or add dev.command to ui-bundle.json to start it automatically', ]); } else { // URL not reachable - we have dev.command (or defaults) to start const devCommand = manifest?.dev?.command ?? DEFAULT_DEV_COMMAND; - if (!selectedWebapp.hasManifest) { + if (!selectedUiBundle.hasManifest) { this.logger.debug(messages.getMessage('info.using-defaults', [devCommand])); } @@ -316,7 +316,7 @@ export default class UiBundleDev extends SfCommand { this.devServerManager = new DevServerManager({ command: devCommand, url: resolvedUrl, - cwd: webappDir, + cwd: uiBundleDir, startupTimeout: 60_000, }); @@ -372,7 +372,7 @@ export default class UiBundleDev extends SfCommand { const suggestions: string[] = [ 'The dev server may be taking longer than expected to start', - 'Check if the dev server command is correct in webapplication.json', + 'Check if the dev server command is correct in ui-bundle.json', `Try running the command manually to see the error: ${devCommand}`, ]; const devError = @@ -403,20 +403,20 @@ export default class UiBundleDev extends SfCommand { // Ensure devServerUrl is set (should always be set by step 3) if (!devServerUrl) { throw new SfError( - '❌ Unable to determine dev server URL. Please specify --url or configure dev.url in webapplication.json.', + '❌ Unable to determine dev server URL. Please specify --url or configure dev.url in ui-bundle.json.', 'DevServerUrlError' ); } // Step 5: Check for Vite proxy and conditionally start standalone proxy - this.logger.debug('Checking if Vite WebApp proxy is active...'); + this.logger.debug('Checking if Vite UI bundle proxy is active...'); const viteProxyActive = await UiBundleDev.checkViteProxyActive(devServerUrl); // Track the final URL to open in browser (either proxy or dev server) let finalUrl: string; if (viteProxyActive) { - // Vite's WebAppProxyHandler is handling the proxy - skip standalone proxy + // Vite's UiBundleProxyHandler is handling the proxy - skip standalone proxy this.log(messages.getMessage('info.vite-proxy-detected', [devServerUrl])); this.logger.debug('Vite proxy detected, skipping standalone proxy server'); finalUrl = devServerUrl; diff --git a/src/config/ManifestWatcher.ts b/src/config/ManifestWatcher.ts index 9612527..8b8e4a0 100644 --- a/src/config/ManifestWatcher.ts +++ b/src/config/ManifestWatcher.ts @@ -19,7 +19,7 @@ import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { watch, type FSWatcher } from 'chokidar'; import { SfError } from '@salesforce/core'; -import type { WebAppManifest } from './manifest.js'; +import type { UiBundleManifest } from './manifest.js'; /** * Manifest change event type @@ -30,7 +30,7 @@ export type ManifestChangeEvent = { /** Path to the manifest file */ path: string; /** New manifest data (if added or changed) */ - manifest?: WebAppManifest; + manifest?: UiBundleManifest; }; /** @@ -38,8 +38,8 @@ export type ManifestChangeEvent = { */ type ManifestWatcherOptions = { /** - * Path to the webapplication.json manifest file - * Defaults to webapplication.json in the current working directory + * Path to the ui-bundle.json manifest file + * Defaults to ui-bundle.json in the current working directory */ manifestPath?: string; @@ -62,14 +62,14 @@ type ManifestWatcherOptions = { type ManifestWatcherEvents = { change: (event: ManifestChangeEvent) => void; error: (error: SfError) => void; - ready: (manifest: WebAppManifest) => void; + ready: (manifest: UiBundleManifest) => void; }; /** - * ManifestWatcher loads and monitors the webapplication.json manifest file + * ManifestWatcher loads and monitors the ui-bundle.json manifest file * * Features: - * - Loads webapplication.json from project root + * - Loads ui-bundle.json from project root * - Watches for file changes and emits events * - Provides helpful error messages * - Supports hot-reload without restarting the proxy @@ -78,7 +78,7 @@ type ManifestWatcherEvents = { export class ManifestWatcher extends EventEmitter { // 1. Instance fields private options: Required; - private manifest: WebAppManifest | null = null; + private manifest: UiBundleManifest | null = null; private watcher: FSWatcher | null = null; private debounceTimeout: NodeJS.Timeout | null = null; @@ -87,7 +87,7 @@ export class ManifestWatcher extends EventEmitter { super(); this.options = { - manifestPath: options.manifestPath ?? join(process.cwd(), 'webapplication.json'), + manifestPath: options.manifestPath ?? join(process.cwd(), 'ui-bundle.json'), watch: options.watch ?? true, debounceMs: options.debounceMs ?? 300, }; @@ -117,7 +117,7 @@ export class ManifestWatcher extends EventEmitter { * * @returns The loaded manifest, or null if not yet loaded */ - public getManifest(): WebAppManifest | null { + public getManifest(): UiBundleManifest | null { return this.manifest; } @@ -175,8 +175,8 @@ export class ManifestWatcher extends EventEmitter { }); this.emit( 'error', - new SfError('webapplication.json was deleted', 'ManifestRemovedError', [ - 'Recreate the webapplication.json file to continue', + new SfError('ui-bundle.json was deleted', 'ManifestRemovedError', [ + 'Recreate the ui-bundle.json file to continue', ]) ); } else { @@ -208,10 +208,10 @@ export class ManifestWatcher extends EventEmitter { private loadManifest(): void { // Check if file exists if (!existsSync(this.options.manifestPath)) { - throw new SfError(`webapplication.json not found at ${this.options.manifestPath}`, 'ManifestNotFoundError', [ + throw new SfError(`ui-bundle.json not found at ${this.options.manifestPath}`, 'ManifestNotFoundError', [ 'Make sure you are in the correct directory', - 'Create a webapplication.json file in your project root', - 'Check that the file is named exactly "webapplication.json"', + 'Create a ui-bundle.json file in your project root', + 'Check that the file is named exactly "ui-bundle.json"', ]); } @@ -221,18 +221,18 @@ export class ManifestWatcher extends EventEmitter { rawContent = readFileSync(this.options.manifestPath, 'utf-8'); } catch (error) { throw new SfError( - `Failed to read webapplication.json: ${error instanceof Error ? error.message : String(error)}`, + `Failed to read ui-bundle.json: ${error instanceof Error ? error.message : String(error)}`, 'ManifestReadError', ['Check file permissions', 'Ensure the file is not locked by another process'] ); } // Parse JSON - let parsed: WebAppManifest; + let parsed: UiBundleManifest; try { - parsed = JSON.parse(rawContent) as WebAppManifest; + parsed = JSON.parse(rawContent) as UiBundleManifest; } catch (error) { - throw new SfError(`Invalid JSON in webapplication.json: ${(error as Error).message}`, 'ManifestParseError', [ + throw new SfError(`Invalid JSON in ui-bundle.json: ${(error as Error).message}`, 'ManifestParseError', [ 'Check for missing commas or brackets', 'Validate JSON syntax using a JSON validator', 'Common issues: trailing commas, unquoted keys, single quotes instead of double quotes', @@ -272,7 +272,7 @@ export class ManifestWatcher extends EventEmitter { this.emit( 'error', new SfError(`File watcher error: ${error.message}`, 'ManifestWatcherError', [ - 'The webapplication.json file watcher encountered an error', + 'The ui-bundle.json file watcher encountered an error', 'You may need to restart the command', ]) ); diff --git a/src/config/manifest.ts b/src/config/manifest.ts index 6506ed7..d3632e0 100644 --- a/src/config/manifest.ts +++ b/src/config/manifest.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -// Re-export base types from @salesforce/webapp-experimental package +// Re-export base types from @salesforce/ui-bundle package export type { - WebAppManifest as BaseWebAppManifest, + UiBundleManifest as BaseUiBundleManifest, RoutingConfig, RewriteRule, RedirectRule, -} from '@salesforce/webapp-experimental/app'; +} from '@salesforce/ui-bundle/app'; // Import for local use -import type { WebAppManifest as BaseWebAppManifest } from '@salesforce/webapp-experimental/app'; +import type { UiBundleManifest as BaseUiBundleManifest } from '@salesforce/ui-bundle/app'; /** * Development configuration (plugin-specific extension) - * NOT in @salesforce/webapp-experimental package + * NOT in @salesforce/ui-bundle package */ export type DevConfig = { /** Command to run the dev server (e.g., "npm run dev") */ @@ -39,10 +39,10 @@ export type DevConfig = { }; /** - * WebApp manifest configuration - defines the structure of webapplication.json file - * Extended from @salesforce/webapp-experimental with plugin-specific fields + * UI bundle manifest configuration - defines the structure of ui-bundle.json file + * Extended from @salesforce/ui-bundle with plugin-specific fields */ -export type WebAppManifest = BaseWebAppManifest & { +export type UiBundleManifest = BaseUiBundleManifest & { /** Development configuration (plugin-specific) */ dev?: DevConfig; }; diff --git a/src/config/types.ts b/src/config/types.ts index 260b2d9..943126d 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -15,7 +15,7 @@ */ // Re-export manifest types from manifest.ts -export type { WebAppManifest, DevConfig, RoutingConfig, RewriteRule, RedirectRule } from './manifest.js'; +export type { UiBundleManifest, DevConfig, RoutingConfig, RewriteRule, RedirectRule } from './manifest.js'; // Re-export from ManifestWatcher export type { ManifestChangeEvent } from './ManifestWatcher.js'; @@ -24,7 +24,7 @@ export type { ManifestChangeEvent } from './ManifestWatcher.js'; * Command execution result * What the sf ui-bundle dev command returns to the user */ -export type WebAppDevResult = { +export type UiBundleDevResult = { /** Proxy server URL (where user should open browser) */ url: string; /** Dev server URL being proxied */ diff --git a/src/config/webappDiscovery.ts b/src/config/webappDiscovery.ts index f3ab702..8664f77 100644 --- a/src/config/webappDiscovery.ts +++ b/src/config/webappDiscovery.ts @@ -17,12 +17,12 @@ import { access, readdir, readFile } from 'node:fs/promises'; import { basename, dirname, join, relative } from 'node:path'; import { Logger, SfError, SfProject } from '@salesforce/core'; -import type { WebAppManifest } from './manifest.js'; +import type { UiBundleManifest } from './manifest.js'; -const logger = Logger.childFromRoot('WebappDiscovery'); +const logger = Logger.childFromRoot('UiBundleDiscovery'); /** - * Default command to run when no webapplication.json manifest is found + * Default command to run when no ui-bundle.json manifest is found */ export const DEFAULT_DEV_COMMAND = 'npm run dev'; @@ -33,27 +33,27 @@ export const DEFAULT_DEV_COMMAND = 'npm run dev'; const WEBAPPLICATIONS_RELATIVE_PATH = 'main/default/uiBundles'; /** - * Pattern to match webapplication metadata XML files + * Pattern to match uibundle metadata XML files */ -const WEBAPP_META_XML_PATTERN = /^(.+)\.webapplication-meta\.xml$/; +const WEBAPP_META_XML_PATTERN = /^(.+)\.uibundle-meta\.xml$/; /** - * Discovered webapp with its directory path and optional manifest + * Discovered uiBundle with its directory path and optional manifest */ -export type DiscoveredWebapp = { - /** Absolute path to the webapp directory */ +export type DiscoveredUiBundle = { + /** Absolute path to the uiBundle directory */ path: string; - /** Relative path from cwd to the webapp directory */ + /** Relative path from cwd to the uiBundle directory */ relativePath: string; - /** Parsed manifest content (null if no webapplication.json found) */ - manifest: WebAppManifest | null; - /** Webapp name (from .webapplication-meta.xml or folder name) */ + /** Parsed manifest content (null if no ui-bundle.json found) */ + manifest: UiBundleManifest | null; + /** Webapp name (from .uibundle-meta.xml or folder name) */ name: string; - /** Whether this webapp has a webapplication.json manifest file */ + /** Whether this uiBundle has a ui-bundle.json manifest file */ hasManifest: boolean; /** Path to the manifest file (null if no manifest) */ manifestPath: string | null; - /** Whether this webapp has a .webapplication-meta.xml file (valid SFDX webapp) */ + /** Whether this uiBundle has a .uibundle-meta.xml file (valid SFDX uiBundle) */ hasMetaXml: boolean; }; @@ -76,16 +76,16 @@ export const UI_BUNDLES_FOLDER = 'uiBundles'; /** * Check if a folder name is the standard uiBundles folder */ -function isWebapplicationsFolder(folderName: string): boolean { +function isUiBundlesFolder(folderName: string): boolean { return folderName === UI_BUNDLES_FOLDER; } /** - * Check if a directory contains a {name}.webapplication-meta.xml file - * Returns the webapp name extracted from the filename, or null if not found. + * Check if a directory contains a {name}.uibundle-meta.xml file + * Returns the uiBundle name extracted from the filename, or null if not found. * Logs a warning if multiple metadata files are found (uses first match). */ -async function findWebappMetaXml(dirPath: string): Promise { +async function findUiBundleMetaXml(dirPath: string): Promise { try { const entries = await readdir(dirPath); const matches: string[] = []; @@ -103,7 +103,7 @@ async function findWebappMetaXml(dirPath: string): Promise { if (matches.length > 1) { logger.warn( - `Multiple .webapplication-meta.xml files found in ${dirPath}: ${matches.join(', ')}. Using "${matches[0]}".` + `Multiple .uibundle-meta.xml files found in ${dirPath}: ${matches.join(', ')}. Using "${matches[0]}".` ); } @@ -126,13 +126,13 @@ async function pathExists(path: string): Promise { } /** - * Try to parse a webapplication.json file. + * Try to parse a ui-bundle.json file. * Accepts any valid JSON object - missing fields will use defaults. */ -async function tryParseWebappManifest(filePath: string): Promise { +async function tryParseUiBundleManifest(filePath: string): Promise { try { const content = await readFile(filePath, 'utf-8'); - const manifest = JSON.parse(content) as WebAppManifest; + const manifest = JSON.parse(content) as UiBundleManifest; // Accept any valid JSON object (missing fields will use defaults) if (manifest && typeof manifest === 'object') { @@ -146,14 +146,14 @@ async function tryParseWebappManifest(filePath: string): Promise folderName. + * Resolve uiBundle name using priority: metaXmlName > folderName. * Manifest does not have a name property - do not depend on it. * * @param folderName - The folder name (fallback) - * @param metaXmlName - Name extracted from .webapplication-meta.xml (or null) - * @returns The resolved webapp name + * @param metaXmlName - Name extracted from .uibundle-meta.xml (or null) + * @returns The resolved uiBundle name */ -function resolveWebappName(folderName: string, metaXmlName: string | null): string { +function resolveUiBundleName(folderName: string, metaXmlName: string | null): string { return metaXmlName ?? folderName; } @@ -180,15 +180,15 @@ async function tryResolveSfdxProjectRoot(cwd: string): Promise { * @param projectRoot - Absolute path to project root (where sfdx-project.json lives) * @returns Array of absolute paths to uiBundles folders that exist */ -async function getWebapplicationsPathsFromProject(projectRoot: string): Promise { +async function getUiBundlesPathsFromProject(projectRoot: string): Promise { try { const project = await SfProject.resolve(projectRoot); const packageDirs = project.getUniquePackageDirectories(); const existenceChecks = await Promise.all( packageDirs.map(async (pkg) => { - const webappsPath = join(projectRoot, pkg.path, WEBAPPLICATIONS_RELATIVE_PATH); - return (await pathExists(webappsPath)) ? webappsPath : null; + const uiBundlesPath = join(projectRoot, pkg.path, WEBAPPLICATIONS_RELATIVE_PATH); + return (await pathExists(uiBundlesPath)) ? uiBundlesPath : null; }) ); @@ -201,26 +201,26 @@ async function getWebapplicationsPathsFromProject(projectRoot: string): Promise< /** * Check if we're inside a uiBundles folder by traversing upward through parent directories. * - * This handles cases where the user runs the command from inside a webapp folder: + * This handles cases where the user runs the command from inside a uiBundle folder: * * Example 1: Running from /project/force-app/main/default/uiBundles/my-app/src/ * Traverses: src -> my-app -> uiBundles (found!) - * Returns: { webappsFolder: "/project/.../uiBundles", currentWebappName: "my-app" } + * Returns: { uiBundlesFolder: "/project/.../uiBundles", currentUiBundleName: "my-app" } * * Example 2: Running from /project/force-app/main/default/uiBundles/my-app/ * Checks parent: uiBundles (found!) - * Returns: { webappsFolder: "/project/.../uiBundles", currentWebappName: "my-app" } + * Returns: { uiBundlesFolder: "/project/.../uiBundles", currentUiBundleName: "my-app" } * * Example 3: Running from /project/force-app/main/default/uiBundles/ * Current dir is uiBundles (found!) - * Returns: { webappsFolder: "/project/.../uiBundles", currentWebappName: null } + * Returns: { uiBundlesFolder: "/project/.../uiBundles", currentUiBundleName: null } * * @param dir - Directory to start from - * @returns Object with uiBundles folder path and current webapp name, or null if not found + * @returns Object with uiBundles folder path and current uiBundle name, or null if not found */ -function findWebapplicationsFolderUpward( +function findUiBundlesFolderUpward( dir: string -): { webappsFolder: string; currentWebappName: string | null } | null { +): { uiBundlesFolder: string; currentUiBundleName: string | null } | null { let currentDir = dir; let childDir: string | null = null; // Tracks the previous dir as we move up const maxUpwardDepth = 10; @@ -233,19 +233,19 @@ function findWebapplicationsFolderUpward( // Case: Current directory IS the uiBundles folder // e.g., cwd = /project/uiBundles - if (isWebapplicationsFolder(dirName)) { + if (isUiBundlesFolder(dirName)) { return { - webappsFolder: currentDir, - currentWebappName: childDir ? basename(childDir) : null, + uiBundlesFolder: currentDir, + currentUiBundleName: childDir ? basename(childDir) : null, }; } // Case: Parent directory is the uiBundles folder // e.g., cwd = /project/uiBundles/my-app (parent is webui) - if (isWebapplicationsFolder(basename(parentDir))) { + if (isUiBundlesFolder(basename(parentDir))) { return { - webappsFolder: parentDir, - currentWebappName: dirName, // Current dir is the webapp folder name + uiBundlesFolder: parentDir, + currentUiBundleName: dirName, // Current dir is the uiBundle folder name }; } @@ -265,53 +265,53 @@ function findWebapplicationsFolderUpward( } /** - * Discover all webapps inside the uiBundles folder. - * Only directories containing a {name}.webapplication-meta.xml file are considered valid webapps. - * If a webapplication.json exists, use it for dev configuration. + * Discover all uiBundles inside the uiBundles folder. + * Only directories containing a {name}.uibundle-meta.xml file are considered valid uiBundles. + * If a ui-bundle.json exists, use it for dev configuration. * - * @param webappsFolderPath - Absolute path to the uiBundles folder + * @param uiBundlesFolderPath - Absolute path to the uiBundles folder * @param cwd - Original working directory for relative path calculation - * @returns Array of discovered webapps (only those with .webapplication-meta.xml) + * @returns Array of discovered uiBundles (only those with .uibundle-meta.xml) */ -async function discoverWebappsInFolder(webappsFolderPath: string, cwd: string): Promise { +async function discoverUiBundlesInFolder(uiBundlesFolderPath: string, cwd: string): Promise { try { - const entries = await readdir(webappsFolderPath, { withFileTypes: true }); + const entries = await readdir(uiBundlesFolderPath, { withFileTypes: true }); - // Get all subdirectories (each is a potential webapp) - const webappDirs = entries.filter((e) => e.isDirectory() && !shouldExcludeDirectory(e.name)); + // Get all subdirectories (each is a potential uiBundle) + const uiBundleDirs = entries.filter((e) => e.isDirectory() && !shouldExcludeDirectory(e.name)); - // Process each webapp directory in parallel - const webappPromises = webappDirs.map(async (entry): Promise => { - const webappPath = join(webappsFolderPath, entry.name); + // Process each uiBundle directory in parallel + const uiBundlePromises = uiBundleDirs.map(async (entry): Promise => { + const uiBundlePath = join(uiBundlesFolderPath, entry.name); - // Check for .webapplication-meta.xml file - this identifies valid webapps - const metaXmlName = await findWebappMetaXml(webappPath); + // Check for .uibundle-meta.xml file - this identifies valid uiBundles + const metaXmlName = await findUiBundleMetaXml(uiBundlePath); - // Only include directories that have a .webapplication-meta.xml file + // Only include directories that have a .uibundle-meta.xml file if (!metaXmlName) { return null; } - const manifestFilePath = join(webappPath, 'webapplication.json'); + const manifestFilePath = join(uiBundlePath, 'ui-bundle.json'); // Try to load manifest for dev configuration - const manifest = await tryParseWebappManifest(manifestFilePath); + const manifest = await tryParseUiBundleManifest(manifestFilePath); return { - path: webappPath, - relativePath: relative(cwd, webappPath) || entry.name, + path: uiBundlePath, + relativePath: relative(cwd, uiBundlePath) || entry.name, manifest, - name: resolveWebappName(entry.name, metaXmlName), + name: resolveUiBundleName(entry.name, metaXmlName), hasManifest: manifest !== null, manifestPath: manifest ? manifestFilePath : null, hasMetaXml: true, }; }); - const results = await Promise.all(webappPromises); + const results = await Promise.all(uiBundlePromises); - // Filter out null results (directories without .webapplication-meta.xml) - return results.filter((webapp): webapp is DiscoveredWebapp => webapp !== null); + // Filter out null results (directories without .uibundle-meta.xml) + return results.filter((uiBundle): uiBundle is DiscoveredUiBundle => uiBundle !== null); } catch { // Permission denied or other read error return []; @@ -319,165 +319,170 @@ async function discoverWebappsInFolder(webappsFolderPath: string, cwd: string): } /** - * Result of finding all webapps, includes context about current location + * Result of finding all uiBundles, includes context about current location */ -type FindAllWebappsResult = { - /** All discovered webapps */ - webapps: DiscoveredWebapp[]; - /** Name of webapp user is currently inside (folder name), null if not inside any */ - currentWebappName: string | null; - /** Whether the uiBundles folder was found (even if empty or no valid webapps) */ - webappsFolderFound: boolean; +type FindAllUiBundlesResult = { + /** All discovered uiBundles */ + uiBundles: DiscoveredUiBundle[]; + /** Name of uiBundle user is currently inside (folder name), null if not inside any */ + currentUiBundleName: string | null; + /** Whether the uiBundles folder was found (even if empty or no valid uiBundles) */ + uiBundlesFolderFound: boolean; /** Whether we're in an SFDX project context */ inSfdxProject: boolean; }; /** - * Find all webapps using simplified discovery algorithm. + * Find all uiBundles using simplified discovery algorithm. * * Discovery strategy (in order): - * 1. Check if inside a uiBundles/ directory (upward search) + * 1. Check if inside a uiBundles/ directory (upward search) * 2. Check for SFDX project and search uiBundles in all package directories - * 3. If neither, check if current directory is a webapp (has .webapplication-meta.xml) + * 3. If neither, check if current directory is a uiBundle (has .uibundle-meta.xml) * * @param cwd - Directory to search from (defaults to process.cwd()) - * @returns Object with discovered webapps and context information + * @returns Object with discovered uiBundles and context information */ -async function findAllWebapps(cwd: string = process.cwd()): Promise { - let webappsFolder: string | null = null; - let currentWebappName: string | null = null; +async function findAllUiBundles(cwd: string = process.cwd()): Promise { + let uiBundlesFolder: string | null = null; + let currentUiBundleName: string | null = null; let inSfdxProject = false; // Step 1: Check if we're inside a uiBundles folder (upward search) - // This handles: running from uiBundles/ or uiBundles//src/ - const upwardResult = findWebapplicationsFolderUpward(cwd); + // This handles: running from uiBundles/ or uiBundles//src/ + const upwardResult = findUiBundlesFolderUpward(cwd); if (upwardResult) { - webappsFolder = upwardResult.webappsFolder; - currentWebappName = upwardResult.currentWebappName; + uiBundlesFolder = upwardResult.uiBundlesFolder; + currentUiBundleName = upwardResult.currentUiBundleName; } else { // Step 2: Check for SFDX project and search uiBundles in all package directories const projectRoot = await tryResolveSfdxProjectRoot(cwd); if (projectRoot) { inSfdxProject = true; - const webappsPaths = await getWebapplicationsPathsFromProject(projectRoot); + const uiBundlesPaths = await getUiBundlesPathsFromProject(projectRoot); - if (webappsPaths.length > 0) { - // Discover webapps from all package directories and combine - const webappArrays = await Promise.all(webappsPaths.map((path) => discoverWebappsInFolder(path, cwd))); - const allWebapps = webappArrays.flat(); + if (uiBundlesPaths.length > 0) { + // Discover uiBundles from all package directories and combine + const uiBundleArrays = await Promise.all(uiBundlesPaths.map((path) => discoverUiBundlesInFolder(path, cwd))); + const allUiBundles = uiBundleArrays.flat(); return { - webapps: allWebapps.sort((a, b) => a.name.localeCompare(b.name)), - currentWebappName: null, - webappsFolderFound: true, + uiBundles: allUiBundles.sort((a, b) => a.name.localeCompare(b.name)), + currentUiBundleName: null, + uiBundlesFolderFound: true, inSfdxProject, }; } } } - // Step 3: If no uiBundles folder found, check if current directory IS a webapp - // (has a .webapplication-meta.xml file) - for running outside SFDX project context - if (!webappsFolder) { - const metaXmlName = await findWebappMetaXml(cwd); + // Step 3: If no uiBundles folder found, check if current directory IS a uiBundle + // (has a .uibundle-meta.xml file) - for running outside SFDX project context + if (!uiBundlesFolder) { + const metaXmlName = await findUiBundleMetaXml(cwd); if (metaXmlName) { - // Current directory is a standalone webapp - const manifestFilePath = join(cwd, 'webapplication.json'); - const manifest = await tryParseWebappManifest(manifestFilePath); - const webappName = resolveWebappName(basename(cwd), metaXmlName); + // Current directory is a standalone uiBundle + const manifestFilePath = join(cwd, 'ui-bundle.json'); + const manifest = await tryParseUiBundleManifest(manifestFilePath); + const uiBundleName = resolveUiBundleName(basename(cwd), metaXmlName); - const standaloneWebapp: DiscoveredWebapp = { + const standaloneUiBundle: DiscoveredUiBundle = { path: cwd, relativePath: '.', manifest, - name: webappName, + name: uiBundleName, hasManifest: manifest !== null, manifestPath: manifest ? manifestFilePath : null, hasMetaXml: true, }; return { - webapps: [standaloneWebapp], - currentWebappName: webappName, - webappsFolderFound: false, + uiBundles: [standaloneUiBundle], + currentUiBundleName: uiBundleName, + uiBundlesFolderFound: false, inSfdxProject: false, }; } - // No webapp found anywhere + // No uiBundle found anywhere return { - webapps: [], - currentWebappName: null, - webappsFolderFound: false, + uiBundles: [], + currentUiBundleName: null, + uiBundlesFolderFound: false, inSfdxProject, }; } - // Discover all webapps in the folder - const webapps = await discoverWebappsInFolder(webappsFolder, cwd); + // Discover all uiBundles in the folder + const uiBundles = await discoverUiBundlesInFolder(uiBundlesFolder, cwd); // Sort by name for consistent ordering return { - webapps: webapps.sort((a, b) => a.name.localeCompare(b.name)), - currentWebappName, - webappsFolderFound: true, + uiBundles: uiBundles.sort((a, b) => a.name.localeCompare(b.name)), + currentUiBundleName, + uiBundlesFolderFound: true, inSfdxProject, }; } /** - * Result of webapp discovery + * Result of uiBundle discovery */ -export type DiscoverWebappResult = { - /** The selected/discovered webapp (null if user needs to select via prompt) */ - webapp: DiscoveredWebapp | null; - /** All discovered webapps */ - allWebapps: DiscoveredWebapp[]; - /** Whether the webapp was auto-selected because user is inside its folder */ +export type DiscoverUiBundleResult = { + /** The selected/discovered uiBundle (null if user needs to select via prompt) */ + uiBundle: DiscoveredUiBundle | null; + /** All discovered uiBundles */ + allUiBundles: DiscoveredUiBundle[]; + /** Whether the uiBundle was auto-selected because user is inside its folder */ autoSelected: boolean; }; /** - * Get a single webapp, handling the various discovery scenarios. + * Get a single uiBundle, handling the various discovery scenarios. * * Discovery use cases: * 1. SFDX Project Root: Search uiBundles in all package directories - * - Webapps identified by {name}.webapplication-meta.xml - * - Always prompt for selection (even if only 1 webapp) + * - Webapps identified by {name}.uibundle-meta.xml + * - Always prompt for selection (even if only 1 uiBundle) * - * 2. Inside uiBundles/ directory: - * - Auto-select current webapp + * 2. Inside uiBundles/ directory: + * - Auto-select current uiBundle * - Error if --name conflicts with current directory * - * 3. Outside SFDX project with .webapplication-meta.xml in current dir: - * - Use current directory as standalone webapp + * 3. Outside SFDX project with .uibundle-meta.xml in current dir: + * - Use current directory as standalone uiBundle * - * @param name - Optional webapp name to search for (--name flag) + * @param name - Optional uiBundle name to search for (--name flag) * @param cwd - Directory to search from - * @returns Object containing the discovered webapp, all webapps, and autoSelected flag - * @throws SfError if no webapps found, named webapp not found, or --name conflicts with current dir + * @returns Object containing the discovered uiBundle, all uiBundles, and autoSelected flag + * @throws SfError if no uiBundles found, named uiBundle not found, or --name conflicts with current dir */ -export async function discoverWebapp( +export async function discoverUiBundle( name: string | undefined, cwd: string = process.cwd() -): Promise { - const { webapps: allWebapps, currentWebappName, webappsFolderFound, inSfdxProject } = await findAllWebapps(cwd); +): Promise { + const { + uiBundles: allUiBundles, + currentUiBundleName, + uiBundlesFolderFound, + inSfdxProject, + } = await findAllUiBundles(cwd); - // No webapps found - if (allWebapps.length === 0) { - if (webappsFolderFound) { - // Folder exists but no valid webapps (no .webapplication-meta.xml files) + // No uiBundles found + if (allUiBundles.length === 0) { + if (uiBundlesFolderFound) { + // Folder exists but no valid uiBundles (no .uibundle-meta.xml files) throw new SfError( - 'Found "uiBundles" folder but no valid webapps inside it.\n' + - 'Each webapp must have a {name}.webapplication-meta.xml file.\n\n' + + 'Found "uiBundles" folder but no valid uiBundles inside it.\n' + + 'Each uiBundle must have a {name}.uibundle-meta.xml file.\n\n' + 'Expected structure:\n' + ' uiBundles/\n' + ' └── my-app/\n' + - ' β”œβ”€β”€ my-app.webapplication-meta.xml (required)\n' + - ' └── webapplication.json (optional, for dev config)', - 'WebappNotFoundError' + ' β”œβ”€β”€ my-app.uibundle-meta.xml (required)\n' + + ' └── ui-bundle.json (optional, for dev config)', + 'UiBundleNotFoundError' ); } else if (inSfdxProject) { // In SFDX project but uiBundles folder doesn't exist @@ -486,67 +491,69 @@ export async function discoverWebapp( 'Create the folder structure in any package directory (e.g. force-app, packages/my-pkg):\n' + ' /main/default/uiBundles/\n' + ' └── my-app/\n' + - ' β”œβ”€β”€ my-app.webapplication-meta.xml (required)\n' + - ' └── webapplication.json (optional, for dev config)', - 'WebappNotFoundError' + ' β”œβ”€β”€ my-app.uibundle-meta.xml (required)\n' + + ' └── ui-bundle.json (optional, for dev config)', + 'UiBundleNotFoundError' ); } else { - // Not in SFDX project and no webapp found + // Not in SFDX project and no uiBundle found throw new SfError( - 'No webapp found.\n\n' + + 'No uiBundle found.\n\n' + 'To use this command, either:\n' + - '1. Run from an SFDX project with webapps in /main/default/uiBundles/\n' + - '2. Run from inside a uiBundles// directory\n' + - '3. Run from a directory containing a {name}.webapplication-meta.xml file', - 'WebappNotFoundError' + '1. Run from an SFDX project with uiBundles in /main/default/uiBundles/\n' + + '2. Run from inside a uiBundles// directory\n' + + '3. Run from a directory containing a {name}.uibundle-meta.xml file', + 'UiBundleNotFoundError' ); } } // Check for --name conflict with current directory - // If user is inside webapp A but specifies --name B, that's an error - if (name && currentWebappName) { - const currentWebapp = allWebapps.find( - (w) => w.name === currentWebappName || basename(w.path) === currentWebappName + // If user is inside uiBundle A but specifies --name B, that's an error + if (name && currentUiBundleName) { + const currentUiBundle = allUiBundles.find( + (w) => w.name === currentUiBundleName || basename(w.path) === currentUiBundleName ); - if (currentWebapp && currentWebapp.name !== name && basename(currentWebapp.path) !== name) { + if (currentUiBundle && currentUiBundle.name !== name && basename(currentUiBundle.path) !== name) { throw new SfError( - `You are inside the "${currentWebappName}" webapp directory but specified --name "${name}".\n\n` + + `You are inside the "${currentUiBundleName}" uiBundle directory but specified --name "${name}".\n\n` + 'Either:\n' + - ` - Remove the --name flag to use the current webapp ("${currentWebappName}")\n` + - ` - Navigate to the "${name}" webapp directory and run the command from there\n` + + ` - Remove the --name flag to use the current uiBundle ("${currentUiBundleName}")\n` + + ` - Navigate to the "${name}" uiBundle directory and run the command from there\n` + ' - Run the command from the project root to use --name', - 'WebappNameConflictError' + 'UiBundleNameConflictError' ); } } - // Priority 1: If --name flag provided, find that specific webapp + // Priority 1: If --name flag provided, find that specific uiBundle if (name) { - const webapp = allWebapps.find((w) => w.name === name || basename(w.path) === name); - if (!webapp) { + const uiBundle = allUiBundles.find((w) => w.name === name || basename(w.path) === name); + if (!uiBundle) { const WARNING = '\u26A0\uFE0F'; // ⚠️ - const availableNames = allWebapps + const availableNames = allUiBundles .map((w) => ` - ${w.name} (${w.relativePath})${w.hasManifest ? '' : ` ${WARNING} No dev manifest`}`) .join('\n'); throw new SfError( - `No webapp found with name "${name}".\n\nAvailable webapps:\n${availableNames}`, - 'WebappNameNotFoundError' + `No uiBundle found with name "${name}".\n\nAvailable uiBundles:\n${availableNames}`, + 'UiBundleNameNotFoundError' ); } - return { webapp, allWebapps, autoSelected: false }; + return { uiBundle, allUiBundles, autoSelected: false }; } - // Priority 2: If user is inside a webapp folder, auto-select that webapp - if (currentWebappName) { - const webapp = allWebapps.find((w) => w.name === currentWebappName || basename(w.path) === currentWebappName); - if (webapp) { - return { webapp, allWebapps, autoSelected: true }; + // Priority 2: If user is inside a uiBundle folder, auto-select that uiBundle + if (currentUiBundleName) { + const uiBundle = allUiBundles.find( + (w) => w.name === currentUiBundleName || basename(w.path) === currentUiBundleName + ); + if (uiBundle) { + return { uiBundle, allUiBundles, autoSelected: true }; } } // No auto-selection - always prompt user to select - // (Removed: auto-selection of single webapp - reviewer wants prompt even for 1 webapp) - return { webapp: null, allWebapps, autoSelected: false }; + // (Removed: auto-selection of single uiBundle - reviewer wants prompt even for 1 uiBundle) + return { uiBundle: null, allUiBundles, autoSelected: false }; } diff --git a/src/proxy/ProxyServer.ts b/src/proxy/ProxyServer.ts index 3a9adfe..5ec2b96 100644 --- a/src/proxy/ProxyServer.ts +++ b/src/proxy/ProxyServer.ts @@ -17,18 +17,14 @@ import { createServer } from 'node:http'; import type { Server, IncomingMessage, ServerResponse } from 'node:http'; import { EventEmitter } from 'node:events'; -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; import httpProxy from 'http-proxy'; import { Logger, SfError } from '@salesforce/core'; -import type { OrgInfo } from '@salesforce/webapp-experimental/app'; -import { getOrgInfo } from '@salesforce/webapp-experimental/app'; -import type { ProxyHandler } from '@salesforce/webapp-experimental/proxy'; -import { createProxyHandler } from '@salesforce/webapp-experimental/proxy'; -import type { WebAppManifest } from '../config/manifest.js'; +import type { OrgInfo } from '@salesforce/ui-bundle/app'; +import { getOrgInfo } from '@salesforce/ui-bundle/app'; +import type { ProxyHandler } from '@salesforce/ui-bundle/proxy'; +import { createProxyHandler } from '@salesforce/ui-bundle/proxy'; +import type { UiBundleManifest } from '../config/manifest.js'; import type { DevServerError } from '../config/types.js'; -import type { ErrorPageData } from '../templates/ErrorPageRenderer.js'; -import { ErrorPageRenderer } from '../templates/ErrorPageRenderer.js'; /** * Configuration for the proxy server @@ -37,7 +33,7 @@ type ProxyServerConfig = { port: number; devServerUrl: string; salesforceInstanceUrl: string; - manifest?: WebAppManifest; + manifest?: UiBundleManifest; orgAlias?: string; host?: string; }; @@ -51,12 +47,10 @@ export class ProxyServer extends EventEmitter { private config: ProxyServerConfig; private readonly logger: Logger; private readonly wsProxy: httpProxy; - private readonly errorPageRenderer: ErrorPageRenderer; private server: Server | null = null; private isCodeBuilder = false; private healthCheckInterval: NodeJS.Timeout | null = null; private devServerStatus: 'unknown' | 'up' | 'down' | 'error' = 'unknown'; - private readonly workspaceScript: string; private activeDevServerError: DevServerError | null = null; private readonly activeConnections: Set = new Set(); private proxyHandler: ProxyHandler | null = null; @@ -67,9 +61,6 @@ export class ProxyServer extends EventEmitter { super(); this.config = config; this.logger = Logger.childFromRoot('ProxyServer'); - this.errorPageRenderer = new ErrorPageRenderer(); - this.workspaceScript = ProxyServer.detectWorkspaceScript(); - this.isCodeBuilder = ProxyServer.detectCodeBuilder(); this.wsProxy = httpProxy.createProxyServer({ @@ -89,25 +80,6 @@ export class ProxyServer extends EventEmitter { return codeBuilderIndicators.some((indicator) => process.env[indicator] !== undefined); } - private static detectWorkspaceScript(): string { - try { - const packageJsonPath = join(process.cwd(), 'package.json'); - const packageJsonContent = readFileSync(packageJsonPath, 'utf-8'); - const packageJson = JSON.parse(packageJsonContent) as { scripts?: Record }; - - const commonScripts = ['dev', 'start', 'serve']; - for (const scriptName of commonScripts) { - if (packageJson.scripts?.[scriptName]) { - return `npm run ${scriptName}`; - } - } - - return 'npm run dev'; - } catch { - return 'npm run dev'; - } - } - /** * Wraps the response to inject a route-change script into HTML pages. * When the app is embedded in Live Preview iframe, this script notifies the parent @@ -372,7 +344,7 @@ export class ProxyServer extends EventEmitter { }); } - public updateManifest(manifest: WebAppManifest): void { + public updateManifest(manifest: UiBundleManifest): void { this.config.manifest = manifest; this.initializeProxyHandler(); this.logger.debug('Proxy handler reinitialized with updated manifest'); @@ -492,8 +464,8 @@ export class ProxyServer extends EventEmitter { } private initializeProxyHandler(): void { - const manifest: WebAppManifest = this.config.manifest ?? { - name: 'webapp', + const manifest: UiBundleManifest = this.config.manifest ?? { + name: 'uiBundle', outputDir: 'dist', }; @@ -503,49 +475,22 @@ export class ProxyServer extends EventEmitter { } private serveDevServerErrorPage(error: DevServerError, res: ServerResponse): void { - try { - const html = this.errorPageRenderer.renderDevServerError(error); - - res.writeHead(500, { - 'Content-Type': 'text/html', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - }); - - res.end(html); - this.logger.debug('Served dev server error page to browser'); - } catch (err) { - this.logger.error(`Failed to render dev server error page: ${err instanceof Error ? err.message : String(err)}`); - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end(`Dev Server Error: ${error.title}\n\n${error.message}\n\nCheck terminal for details.`); - } + res.writeHead(500, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + }); + res.end(`Dev Server Error: ${error.title}\n\n${error.message}\n\nCheck terminal for details.`); + this.logger.debug('Served dev server error response'); } private serveErrorPage(res: ServerResponse): void { - try { - const errorPageData: ErrorPageData = { - status: 'No Dev Server Detected', - devServerUrl: this.config.devServerUrl, - workspaceScript: this.workspaceScript, - proxyUrl: this.getProxyUrl(), - orgTarget: this.config.salesforceInstanceUrl.replace('https://', ''), - }; - - const html = this.errorPageRenderer.render(errorPageData); - - res.writeHead(503, { - 'Content-Type': 'text/html', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - }); - res.end(html); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`CRITICAL: Failed to render dev server error page: ${errorMessage}`); - - res.writeHead(503, { 'Content-Type': 'text/plain' }); - res.end( - `Dev Server Unavailable\n\nCannot connect to: ${this.config.devServerUrl}\n\nStart your dev server and refresh this page.` - ); - } + res.writeHead(503, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + }); + res.end( + `Dev Server Unavailable\n\nCannot connect to: ${this.config.devServerUrl}\n\nStart your dev server and refresh this page.` + ); } private startHealthCheck(): void { diff --git a/src/server/DevServerManager.ts b/src/server/DevServerManager.ts index e0544ab..ad7c919 100644 --- a/src/server/DevServerManager.ts +++ b/src/server/DevServerManager.ts @@ -359,7 +359,7 @@ export class DevServerManager extends EventEmitter { this.logger.error(`Dev server process error: ${error.message}`); const sfError = new SfError(`❌ Dev server process error: ${error.message}`, 'DevServerProcessError', [ - 'Check that the command is correct in webapplication.json', + 'Check that the command is correct in ui-bundle.json', 'Verify all dependencies are installed', 'Try running the command manually to see the error', ]); diff --git a/src/templates/ErrorPageRenderer.ts b/src/templates/ErrorPageRenderer.ts deleted file mode 100644 index a33aae7..0000000 --- a/src/templates/ErrorPageRenderer.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2026, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getErrorPageTemplate } from '@salesforce/webapp-experimental/proxy'; -import type { DevServerError } from '../config/types.js'; - -export type ErrorPageData = { - status: string; - devServerUrl: string; - workspaceScript: string; - proxyUrl: string; - orgTarget: string; -}; - -/** - * Renders HTML error pages for browser display when dev server is unavailable - * or when runtime errors occur - * - * Uses a single template with conditional sections for all error types - */ -export class ErrorPageRenderer { - private template: string; - - public constructor() { - this.template = getErrorPageTemplate(); - } - - /** - * Escape HTML special characters for safe display - * - * @param text - Text to escape - * @returns Escaped HTML - */ - private static escapeHtml(text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - /** - * Render a simple dev server down error page - * - * @param data - The data to inject into the template - * @returns Rendered HTML string - */ - public render(data: ErrorPageData): string { - const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false }); - - // Extract port from proxy URL (e.g., "http://localhost:4545" -> "4545") - const proxyPort = new URL(data.proxyUrl).port || '4545'; - - return ( - this.template - // Page metadata - .replace(/\{\{PAGE_TITLE\}\}/g, 'Dev Server Unavailable') - .replace(/\{\{META_REFRESH\}\}/g, '') - - // Header - .replace(/\{\{ERROR_TITLE\}\}/g, 'No Dev Server Detected') - .replace(/\{\{STATUS_CLASS\}\}/g, 'warning') - .replace(/\{\{ERROR_STATUS\}\}/g, ErrorPageRenderer.escapeHtml(data.status)) - - // Message content - .replace( - /\{\{MESSAGE_CONTENT\}\}/g, - ` -

The proxy cannot connect to your dev server. This usually means:

-
    -
  • Your dev server isn't running yet
  • -
  • The dev server is starting up (please wait)
  • -
  • The dev server crashed or exited
  • -
- ` - ) - - // Diagnostics data - .replace(/\{\{DEV_SERVER_URL\}\}/g, ErrorPageRenderer.escapeHtml(data.devServerUrl)) - .replace(/\{\{PROXY_URL\}\}/g, ErrorPageRenderer.escapeHtml(data.proxyUrl)) - .replace(/\{\{PROXY_PORT\}\}/g, proxyPort) - .replace(/\{\{ORG_TARGET\}\}/g, ErrorPageRenderer.escapeHtml(data.orgTarget)) - .replace(/\{\{WORKSPACE_SCRIPT\}\}/g, ErrorPageRenderer.escapeHtml(data.workspaceScript)) - .replace(/\{\{LAST_CHECK_TIME\}\}/g, timestamp) - - // Section visibility (show simple, hide others) - .replace(/\{\{SIMPLE_SECTION_CLASS\}\}/g, '') - .replace(/\{\{RUNTIME_SECTION_CLASS\}\}/g, 'hidden') - .replace(/\{\{DEV_SERVER_SECTION_CLASS\}\}/g, 'hidden') - .replace(/\{\{SUGGESTIONS_SECTION_CLASS\}\}/g, '') - - // Auto-refresh - .replace(/\{\{AUTO_REFRESH_CLASS\}\}/g, '') - .replace(/\{\{AUTO_REFRESH_TEXT\}\}/g, 'Auto-refreshing every 3 seconds...') - ); - } - - /** - * Render a dev server error page with stderr output and suggestions - * - * @param error - Parsed dev server error - * @returns Rendered HTML string - */ - public renderDevServerError(error: DevServerError): string { - // Format suggestions list (just the
  • items, structure is in template) - const suggestionsList = error.suggestions.map((s) => `
  • ${ErrorPageRenderer.escapeHtml(s)}
  • `).join('\n'); - - // Format stderr lines with proper escaping (just the text content) - const stderrOutput = error.stderrLines.map((line) => ErrorPageRenderer.escapeHtml(line)).join('\n'); - - // Use default proxy port for emergency commands - const proxyPort = '4545'; - - const html = this.template - // Page metadata - .replace(/\{\{PAGE_TITLE\}\}/g, 'Dev Server Error') - .replace(/\{\{META_REFRESH\}\}/g, '') - - // Header - .replace(/\{\{ERROR_TITLE\}\}/g, ErrorPageRenderer.escapeHtml(error.title)) - .replace(/\{\{STATUS_CLASS\}\}/g, 'error') - .replace(/\{\{ERROR_STATUS\}\}/g, 'Error Detected') - - // Message content - .replace(/\{\{ERROR_MESSAGE_TEXT\}\}/g, ErrorPageRenderer.escapeHtml(error.message)) - - // Dev server error data - .replace(/\{\{STDERR_OUTPUT\}\}/g, stderrOutput) - .replace(/\{\{PROXY_PORT\}\}/g, proxyPort) - - // Suggestions - .replace(/\{\{SUGGESTIONS_TITLE\}\}/g, 'How to Fix This') - .replace(/\{\{SUGGESTIONS_LIST\}\}/g, suggestionsList) - - // Section visibility (show dev server error, hide others) - .replace(/\{\{SIMPLE_SECTION_CLASS\}\}/g, 'hidden') - .replace(/\{\{RUNTIME_SECTION_CLASS\}\}/g, 'hidden') - .replace(/\{\{DEV_SERVER_SECTION_CLASS\}\}/g, '') - .replace(/\{\{SUGGESTIONS_SECTION_CLASS\}\}/g, '') - - // Auto-refresh - .replace(/\{\{AUTO_REFRESH_CLASS\}\}/g, '') - .replace( - /\{\{AUTO_REFRESH_TEXT\}\}/g, - 'This page will auto-refresh every 5 seconds until the dev server starts successfully' - ); - - return html; - } -} diff --git a/test/commands/ui-bundle/dev.nut.ts b/test/commands/ui-bundle/dev.nut.ts index a4f7fec..ffcab42 100644 --- a/test/commands/ui-bundle/dev.nut.ts +++ b/test/commands/ui-bundle/dev.nut.ts @@ -21,12 +21,12 @@ import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; import { expect } from 'chai'; import { createProject, - createProjectWithWebapp, - createProjectWithMultipleWebapps, - createEmptyWebappsDir, - createWebappDirWithoutMeta, + createProjectWithUiBundle, + createProjectWithMultipleUiBundles, + createEmptyUiBundlesDir, + createUiBundleDirWithoutMeta, writeManifest, - webappPath, + uiBundlePath, ensureSfCli, authOrgViaUrl, REAL_HOME, @@ -65,7 +65,7 @@ describe('ui-bundle dev NUTs β€” Tier 1 (no auth)', () => { /* ------------------------------------------------------------------ * * Tier 2 β€” CLI Validation (with auth) * * * - * Validates webapp discovery errors and URL resolution errors. * + * Validates uiBundle discovery errors and URL resolution errors. * * Auth is only needed so --target-org passes parsing; these tests * * exercise local filesystem/network checks β€” no live org calls. * * * @@ -93,83 +93,83 @@ describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { // ── Discovery errors ────────────────────────────────────────── - // Project has no uiBundles folder at all β†’ WebappNotFoundError. - it('should error when no webapp found (project only, no webapps)', () => { - const projectDir = createProject(session, 'noWebappProject'); + // Project has no uiBundles folder at all β†’ UiBundleNotFoundError. + it('should error when no uiBundle found (project only, no uiBundles)', () => { + const projectDir = createProject(session, 'noUiBundleProject'); const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); - expect(result.jsonOutput?.name).to.equal('WebappNotFoundError'); + expect(result.jsonOutput?.name).to.equal('UiBundleNotFoundError'); }); - // Project has webapp "realApp" but --name asks for "NonExistent" β†’ WebappNameNotFoundError. - it('should error when --name does not match any webapp', () => { - const projectDir = createProjectWithWebapp(session, 'nameNotFound', 'realApp'); + // Project has uiBundle "realApp" but --name asks for "NonExistent" β†’ UiBundleNameNotFoundError. + it('should error when --name does not match any uiBundle', () => { + const projectDir = createProjectWithUiBundle(session, 'nameNotFound', 'realApp'); const result = execCmd(`ui-bundle dev --name NonExistent --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); - expect(result.jsonOutput?.name).to.equal('WebappNameNotFoundError'); + expect(result.jsonOutput?.name).to.equal('UiBundleNameNotFoundError'); }); - // cwd is inside webapp "appA" but --name asks for "appB" β†’ WebappNameConflictError. + // cwd is inside uiBundle "appA" but --name asks for "appB" β†’ UiBundleNameConflictError. // Discovery treats this as ambiguous intent and rejects it. - it('should error on --name conflict when inside a different webapp', () => { - const projectDir = createProjectWithWebapp(session, 'nameConflict', 'appA'); + it('should error on --name conflict when inside a different uiBundle', () => { + const projectDir = createProjectWithUiBundle(session, 'nameConflict', 'appA'); execSync('sf ui-bundle generate --name appB', { cwd: projectDir, stdio: 'pipe', env: { ...process.env, HOME: REAL_HOME, USERPROFILE: REAL_HOME }, }); - const cwdInsideAppA = webappPath(projectDir, 'appA'); + const cwdInsideAppA = uiBundlePath(projectDir, 'appA'); const result = execCmd(`ui-bundle dev --name appB --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: cwdInsideAppA, }); - expect(result.jsonOutput?.name).to.equal('WebappNameConflictError'); + expect(result.jsonOutput?.name).to.equal('UiBundleNameConflictError'); }); - // uiBundles/ folder exists but is empty β†’ WebappNotFoundError. + // uiBundles/ folder exists but is empty β†’ UiBundleNotFoundError. it('should error when uiBundles folder is empty', () => { - const projectDir = createProject(session, 'emptyWebapps'); - createEmptyWebappsDir(projectDir); + const projectDir = createProject(session, 'emptyUiBundles'); + createEmptyUiBundlesDir(projectDir); const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); - expect(result.jsonOutput?.name).to.equal('WebappNotFoundError'); + expect(result.jsonOutput?.name).to.equal('UiBundleNotFoundError'); }); - // uiBundles/orphanApp/ exists but has no .webapplication-meta.xml β†’ not a valid webapp. - it('should error when webapp dir has no .webapplication-meta.xml', () => { + // uiBundles/orphanApp/ exists but has no .uibundle-meta.xml β†’ not a valid uiBundle. + it('should error when uiBundle dir has no .uibundle-meta.xml', () => { const projectDir = createProject(session, 'noMeta'); - createWebappDirWithoutMeta(projectDir, 'orphanApp'); + createUiBundleDirWithoutMeta(projectDir, 'orphanApp'); const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: projectDir, }); - expect(result.jsonOutput?.name).to.equal('WebappNotFoundError'); + expect(result.jsonOutput?.name).to.equal('UiBundleNotFoundError'); }); - // ── Multiple webapps selection ──────────────────────────────── + // ── Multiple uiBundles selection ──────────────────────────────── // Project has appA and appB. Using --name appA from project root selects - // that webapp and proceeds past discovery. Fails at DevServerUrlError - // (no dev server) β€” confirming named selection works with multiple webapps. - it('should use --name to select one webapp when multiple exist', () => { - const projectDir = createProjectWithMultipleWebapps(session, 'multiSelect', ['appA', 'appB']); + // that uiBundle and proceeds past discovery. Fails at DevServerUrlError + // (no dev server) β€” confirming named selection works with multiple uiBundles. + it('should use --name to select one uiBundle when multiple exist', () => { + const projectDir = createProjectWithMultipleUiBundles(session, 'multiSelect', ['appA', 'appB']); writeManifest(projectDir, 'appA', { dev: { url: 'http://localhost:5180' }, @@ -186,9 +186,9 @@ describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { expect(result.jsonOutput?.name).to.equal('DevServerUrlError'); }); - // Project has appA and appB. Using --name appB selects the second webapp. - it('should use --name to select second webapp when multiple exist', () => { - const projectDir = createProjectWithMultipleWebapps(session, 'multiSelectB', ['appA', 'appB']); + // Project has appA and appB. Using --name appB selects the second uiBundle. + it('should use --name to select second uiBundle when multiple exist', () => { + const projectDir = createProjectWithMultipleUiBundles(session, 'multiSelectB', ['appA', 'appB']); writeManifest(projectDir, 'appA', { dev: { url: 'http://localhost:5182' }, @@ -208,18 +208,18 @@ describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { // ── Auto-selection ──────────────────────────────────────────── // When cwd is inside uiBundles/myApp/, discovery auto-selects that - // webapp without --name. The command proceeds past discovery and fails at + // uiBundle without --name. The command proceeds past discovery and fails at // URL resolution (no dev server running) β€” confirming auto-select worked. - it('should auto-select webapp when run from inside its directory', () => { - const projectDir = createProjectWithWebapp(session, 'autoSelect', 'myApp'); + it('should auto-select uiBundle when run from inside its directory', () => { + const projectDir = createProjectWithUiBundle(session, 'autoSelect', 'myApp'); writeManifest(projectDir, 'myApp', { dev: { url: 'http://localhost:5179' }, }); - const cwdInsideApp = webappPath(projectDir, 'myApp'); + const cwdInsideApp = uiBundlePath(projectDir, 'myApp'); - // No --name flag; cwd is inside the webapp directory. + // No --name flag; cwd is inside the uiBundle directory. // Discovery auto-selects myApp, then the command fails at URL check // (nothing running on 5179). DevServerUrlError proves discovery succeeded. const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { @@ -230,11 +230,11 @@ describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { expect(result.jsonOutput?.name).to.equal('DevServerUrlError'); }); - // When multiple webapps exist and cwd is inside uiBundles/appA/, + // When multiple uiBundles exist and cwd is inside uiBundles/appA/, // discovery auto-selects appA without prompting. Proceeds past discovery // and fails at URL resolution β€” confirming auto-select works with multiple. - it('should auto-select webapp when run from inside its directory (multiple webapps)', () => { - const projectDir = createProjectWithMultipleWebapps(session, 'autoSelectMulti', ['appA', 'appB']); + it('should auto-select uiBundle when run from inside its directory (multiple uiBundles)', () => { + const projectDir = createProjectWithMultipleUiBundles(session, 'autoSelectMulti', ['appA', 'appB']); writeManifest(projectDir, 'appA', { dev: { url: 'http://localhost:5184' }, @@ -243,7 +243,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { dev: { url: 'http://localhost:5185' }, }); - const cwdInsideAppA = webappPath(projectDir, 'appA'); + const cwdInsideAppA = uiBundlePath(projectDir, 'appA'); const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, @@ -258,7 +258,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { // --url explicitly provided but nothing is listening β†’ DevServerUrlError. // The command refuses to start a dev server when --url is given. it('should error when --url is unreachable', () => { - const projectDir = createProjectWithWebapp(session, 'urlUnreachable', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'urlUnreachable', 'myApp'); const result = execCmd(`ui-bundle dev --name myApp --url http://localhost:5179 --target-org ${targetOrg} --json`, { ensureExitCode: 1, @@ -271,7 +271,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { // Manifest has dev.url but no dev.command β†’ command can't start the server // itself and the URL is unreachable β†’ DevServerUrlError. it('should error when dev.url is unreachable and no dev.command', () => { - const projectDir = createProjectWithWebapp(session, 'urlNoCmd', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'urlNoCmd', 'myApp'); writeManifest(projectDir, 'myApp', { dev: { url: 'http://localhost:5179' }, @@ -287,15 +287,15 @@ describe('ui-bundle dev NUTs β€” Tier 2 CLI validation', () => { // ── Dev server startup errors ───────────────────────────────── - // Webapp created but npm install never run β†’ dev server fails because + // UI bundle created but npm install never run β†’ dev server fails because // dependencies (e.g. vite) are not installed. The command should exit // with a meaningful error that suggests installing dependencies. // This mirrors the real user flow: generate β†’ dev (without install). it('should include a reason when dev server fails to start', () => { - const projectDir = createProjectWithWebapp(session, 'noInstall', 'myApp'); - const appDir = webappPath(projectDir, 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'noInstall', 'myApp'); + const appDir = uiBundlePath(projectDir, 'myApp'); - writeFileSync(join(appDir, 'package.json'), JSON.stringify({ name: 'test-webapp', scripts: { dev: 'vite' } })); + writeFileSync(join(appDir, 'package.json'), JSON.stringify({ name: 'test-uiBundle', scripts: { dev: 'vite' } })); writeManifest(projectDir, 'myApp', { dev: { command: 'npm run dev' }, }); diff --git a/test/commands/ui-bundle/dev.test.ts b/test/commands/ui-bundle/dev.test.ts index 4c8db06..626f53e 100644 --- a/test/commands/ui-bundle/dev.test.ts +++ b/test/commands/ui-bundle/dev.test.ts @@ -17,7 +17,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { TestContext } from '@salesforce/core/testSetup'; -import type { WebAppManifest, WebAppDevResult } from '../../../src/config/types.js'; +import type { UiBundleManifest, UiBundleDevResult } from '../../../src/config/types.js'; describe('ui-bundle:dev command integration', () => { const $$ = new TestContext(); @@ -49,15 +49,15 @@ describe('ui-bundle:dev command integration', () => { method: 'GET', signal: AbortSignal.timeout(3000), }); - return response.headers.get('X-Salesforce-WebApp-Proxy') === 'true'; + return response.headers.get('X-Salesforce-UiBundle-Proxy') === 'true'; } catch { return false; } } - it('should return true when X-Salesforce-WebApp-Proxy header is present and true', async () => { + it('should return true when X-Salesforce-UiBundle-Proxy header is present and true', async () => { const mockHeaders = new Headers(); - mockHeaders.set('X-Salesforce-WebApp-Proxy', 'true'); + mockHeaders.set('X-Salesforce-UiBundle-Proxy', 'true'); fetchStub.resolves({ ok: true, @@ -74,9 +74,9 @@ describe('ui-bundle:dev command integration', () => { expect(calledUrl).to.include('sfProxyHealthCheck=true'); }); - it('should return false when X-Salesforce-WebApp-Proxy header is not present', async () => { + it('should return false when X-Salesforce-UiBundle-Proxy header is not present', async () => { const mockHeaders = new Headers(); - // No X-Salesforce-WebApp-Proxy header + // No X-Salesforce-UiBundle-Proxy header fetchStub.resolves({ ok: true, @@ -88,9 +88,9 @@ describe('ui-bundle:dev command integration', () => { expect(result).to.be.false; }); - it('should return false when X-Salesforce-WebApp-Proxy header is present but not "true"', async () => { + it('should return false when X-Salesforce-UiBundle-Proxy header is present but not "true"', async () => { const mockHeaders = new Headers(); - mockHeaders.set('X-Salesforce-WebApp-Proxy', 'false'); + mockHeaders.set('X-Salesforce-UiBundle-Proxy', 'false'); fetchStub.resolves({ ok: true, @@ -128,7 +128,7 @@ describe('ui-bundle:dev command integration', () => { it('should construct correct health check URL with query parameter', async () => { const mockHeaders = new Headers(); - mockHeaders.set('X-Salesforce-WebApp-Proxy', 'true'); + mockHeaders.set('X-Salesforce-UiBundle-Proxy', 'true'); fetchStub.resolves({ ok: true, @@ -143,7 +143,7 @@ describe('ui-bundle:dev command integration', () => { it('should preserve existing query parameters when adding health check', async () => { const mockHeaders = new Headers(); - mockHeaders.set('X-Salesforce-WebApp-Proxy', 'true'); + mockHeaders.set('X-Salesforce-UiBundle-Proxy', 'true'); fetchStub.resolves({ ok: true, @@ -172,21 +172,21 @@ describe('ui-bundle:dev command integration', () => { }); describe('Type Definitions', () => { - it('should have correct WebAppManifest structure', () => { - const manifest: WebAppManifest = { - name: 'testWebApp', + it('should have correct UiBundleManifest structure', () => { + const manifest: UiBundleManifest = { + name: 'testUiBundle', outputDir: 'dist', dev: { url: 'http://localhost:5173', }, }; - expect(manifest.name).to.equal('testWebApp'); + expect(manifest.name).to.equal('testUiBundle'); expect(manifest.dev?.url).to.equal('http://localhost:5173'); }); - it('should have correct WebAppDevResult structure', () => { - const result: WebAppDevResult = { + it('should have correct UiBundleDevResult structure', () => { + const result: UiBundleDevResult = { url: 'http://localhost:4545', devServerUrl: 'http://localhost:5173', }; @@ -206,8 +206,8 @@ describe('ui-bundle:dev command integration', () => { }); it('should use manifest dev.url when no explicit URL', () => { - const manifest: WebAppManifest = { - name: 'testWebApp', + const manifest: UiBundleManifest = { + name: 'testUiBundle', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -218,8 +218,8 @@ describe('ui-bundle:dev command integration', () => { }); it('should use dev.command when no URL provided', () => { - const manifest: WebAppManifest = { - name: 'testWebApp', + const manifest: UiBundleManifest = { + name: 'testUiBundle', outputDir: 'dist', dev: { command: 'npm run dev', @@ -232,8 +232,8 @@ describe('ui-bundle:dev command integration', () => { describe('Configuration Validation', () => { it('should validate manifest with dev.url', () => { - const manifest: WebAppManifest = { - name: 'testWebApp', + const manifest: UiBundleManifest = { + name: 'testUiBundle', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -246,8 +246,8 @@ describe('ui-bundle:dev command integration', () => { }); it('should validate manifest with dev.command', () => { - const manifest: WebAppManifest = { - name: 'testWebApp', + const manifest: UiBundleManifest = { + name: 'testUiBundle', outputDir: 'dist', dev: { command: 'npm run dev', diff --git a/test/commands/ui-bundle/devPort.nut.ts b/test/commands/ui-bundle/devPort.nut.ts index 054358c..c4fa667 100644 --- a/test/commands/ui-bundle/devPort.nut.ts +++ b/test/commands/ui-bundle/devPort.nut.ts @@ -20,12 +20,12 @@ import { expect } from 'chai'; import { createProjectWithDevServer, ensureSfCli, authOrgViaUrl } from './helpers/webappProjectUtils.js'; import { occupyPort, - spawnWebappDev, + spawnUiBundleDev, closeServer, SUITE_TIMEOUT, SPAWN_TIMEOUT, SPAWN_FAIL_TIMEOUT, - type WebappDevHandle, + type UiBundleDevHandle, } from './helpers/devServerUtils.js'; /* ------------------------------------------------------------------ * @@ -48,7 +48,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 port handling', function () { let session: TestSession; let targetOrg: string; let blocker: Server | null = null; - let handle: WebappDevHandle | null = null; + let handle: UiBundleDevHandle | null = null; before(async () => { if (!process.env.TESTKIT_AUTH_URL) { @@ -83,7 +83,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 port handling', function () { const { projectDir } = createProjectWithDevServer(session, 'portConflict', 'myApp', DEV_PORT); try { - handle = await spawnWebappDev(['--name', 'myApp', '--port', String(PROXY_PORT), '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--port', String(PROXY_PORT), '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_FAIL_TIMEOUT, }); @@ -101,7 +101,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 port handling', function () { blocker = await occupyPort(PROXY_PORT + 1); try { - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_FAIL_TIMEOUT, }); @@ -118,7 +118,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 port handling', function () { const { projectDir } = createProjectWithDevServer(session, 'portAutoInc', 'myApp', DEV_PORT + 2); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -133,7 +133,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 port handling', function () { const { projectDir } = createProjectWithDevServer(session, 'customPort', 'myApp', DEV_PORT + 3); - handle = await spawnWebappDev(['--name', 'myApp', '--port', String(customPort), '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--port', String(customPort), '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -148,7 +148,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 port handling', function () { const { projectDir } = createProjectWithDevServer(session, 'manifestPortOk', 'myApp', DEV_PORT + 4, manifestPort); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); diff --git a/test/commands/ui-bundle/devWithUrl.nut.ts b/test/commands/ui-bundle/devWithUrl.nut.ts index df3740d..b976b76 100644 --- a/test/commands/ui-bundle/devWithUrl.nut.ts +++ b/test/commands/ui-bundle/devWithUrl.nut.ts @@ -19,19 +19,19 @@ import { TestSession } from '@salesforce/cli-plugins-testkit'; import { expect } from 'chai'; import { createProjectWithDevServer, - createProjectWithWebapp, + createProjectWithUiBundle, writeManifest, ensureSfCli, authOrgViaUrl, } from './helpers/webappProjectUtils.js'; import { - spawnWebappDev, + spawnUiBundleDev, startTestHttpServer, startViteProxyServer, closeServer, SUITE_TIMEOUT, SPAWN_TIMEOUT, - type WebappDevHandle, + type UiBundleDevHandle, } from './helpers/devServerUtils.js'; /* ------------------------------------------------------------------ * @@ -55,7 +55,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { let session: TestSession; let targetOrg: string; - let handle: WebappDevHandle | null = null; + let handle: UiBundleDevHandle | null = null; let externalServer: HttpServer | null = null; before(async () => { @@ -93,7 +93,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { it('should start proxy when dev.command starts a dev server', async () => { const { projectDir } = createProjectWithDevServer(session, 'fullFlow', 'myApp', FULL_FLOW_PORT); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -107,7 +107,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { it('should serve proxied content from the dev server', async () => { const { projectDir } = createProjectWithDevServer(session, 'proxyContent', 'myApp', FULL_FLOW_PORT + 1); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -124,7 +124,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { it('should emit JSON with proxy URL on stderr', async () => { const { projectDir } = createProjectWithDevServer(session, 'jsonOutput', 'myApp', FULL_FLOW_PORT + 2); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -149,10 +149,10 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { const defaultDevPort = 5173; externalServer = await startTestHttpServer(defaultDevPort); - const projectDir = createProjectWithWebapp(session, 'emptyManifest', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'emptyManifest', 'myApp'); writeManifest(projectDir, 'myApp', {}); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -172,9 +172,9 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { it('should start proxy when --url points to an already-running server', async () => { externalServer = await startTestHttpServer(PROXY_ONLY_PORT); - const projectDir = createProjectWithWebapp(session, 'proxyOnly', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'proxyOnly', 'myApp'); - handle = await spawnWebappDev( + handle = await spawnUiBundleDev( ['--name', 'myApp', '--url', `http://localhost:${PROXY_ONLY_PORT}`, '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT } ); @@ -189,9 +189,9 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { it('should serve proxied content from the external server via --url', async () => { externalServer = await startTestHttpServer(PROXY_ONLY_PORT + 1); - const projectDir = createProjectWithWebapp(session, 'proxyOnlyContent', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'proxyOnlyContent', 'myApp'); - handle = await spawnWebappDev( + handle = await spawnUiBundleDev( ['--name', 'myApp', '--url', `http://localhost:${PROXY_ONLY_PORT + 1}`, '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT } ); @@ -208,12 +208,12 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { it('should start proxy when dev.url in manifest is already reachable (no dev.command needed)', async () => { externalServer = await startTestHttpServer(PROXY_ONLY_PORT + 2); - const projectDir = createProjectWithWebapp(session, 'proxyOnlyManifest', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'proxyOnlyManifest', 'myApp'); writeManifest(projectDir, 'myApp', { dev: { url: `http://localhost:${PROXY_ONLY_PORT + 2}` }, }); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -233,9 +233,9 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { const customProxyPort = PROXY_ONLY_PORT + 10; externalServer = await startTestHttpServer(PROXY_ONLY_PORT + 3); - const projectDir = createProjectWithWebapp(session, 'proxyOnlyPort', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'proxyOnlyPort', 'myApp'); - handle = await spawnWebappDev( + handle = await spawnUiBundleDev( [ '--name', 'myApp', @@ -258,7 +258,7 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { // ── Vite proxy mode (dev server has built-in proxy) ────────────── // When the dev server responds to ?sfProxyHealthCheck=true with the - // X-Salesforce-WebApp-Proxy header, the command skips the standalone + // X-Salesforce-UiBundle-Proxy header, the command skips the standalone // proxy and uses the dev server URL directly. describe('Vite proxy mode', () => { @@ -267,12 +267,12 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { it('should skip standalone proxy when Vite proxy is detected', async () => { externalServer = await startViteProxyServer(VITE_PORT); - const projectDir = createProjectWithWebapp(session, 'viteProxy', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'viteProxy', 'myApp'); writeManifest(projectDir, 'myApp', { dev: { url: `http://localhost:${VITE_PORT}` }, }); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -284,12 +284,12 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { it('should serve content directly from Vite server (no standalone proxy)', async () => { externalServer = await startViteProxyServer(VITE_PORT + 1); - const projectDir = createProjectWithWebapp(session, 'viteContent', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'viteContent', 'myApp'); writeManifest(projectDir, 'myApp', { dev: { url: `http://localhost:${VITE_PORT + 1}` }, }); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); @@ -301,17 +301,17 @@ describe('ui-bundle dev NUTs β€” Tier 2 URL/proxy integration', function () { expect(body).to.include('Vite Dev Server'); }); - // Server responds to health check but WITHOUT the X-Salesforce-WebApp-Proxy + // Server responds to health check but WITHOUT the X-Salesforce-UiBundle-Proxy // header β†’ standalone proxy starts as usual (fallback path). it('should start standalone proxy when server lacks Vite proxy header', async () => { externalServer = await startTestHttpServer(VITE_PORT + 2); - const projectDir = createProjectWithWebapp(session, 'noViteProxy', 'myApp'); + const projectDir = createProjectWithUiBundle(session, 'noViteProxy', 'myApp'); writeManifest(projectDir, 'myApp', { dev: { url: `http://localhost:${VITE_PORT + 2}` }, }); - handle = await spawnWebappDev(['--name', 'myApp', '--target-org', targetOrg], { + handle = await spawnUiBundleDev(['--name', 'myApp', '--target-org', targetOrg], { cwd: projectDir, timeout: SPAWN_TIMEOUT, }); diff --git a/test/commands/ui-bundle/helpers/devServerUtils.ts b/test/commands/ui-bundle/helpers/devServerUtils.ts index 40b7a1f..3b5aac5 100644 --- a/test/commands/ui-bundle/helpers/devServerUtils.ts +++ b/test/commands/ui-bundle/helpers/devServerUtils.ts @@ -32,13 +32,13 @@ import { createServer, type Server } from 'node:net'; /** Mocha suite-level timeout for describe blocks that spawn webapp dev. */ export const SUITE_TIMEOUT = 180_000; -/** Timeout for spawnWebappDev when the command is expected to start successfully. */ +/** Timeout for spawnUiBundleDev when the command is expected to start successfully. */ export const SPAWN_TIMEOUT = 120_000; -/** Shorter timeout for spawnWebappDev when the command is expected to fail quickly. */ +/** Shorter timeout for spawnUiBundleDev when the command is expected to fail quickly. */ export const SPAWN_FAIL_TIMEOUT = 60_000; -export type WebappDevHandle = { +export type UiBundleDevHandle = { /** The proxy URL emitted by the command on stderr as JSON `{"url":"..."}` */ proxyUrl: string; /** The underlying child process */ @@ -55,7 +55,10 @@ export type WebappDevHandle = { * Uses `bin/dev.js` (same binary that `execCmd` uses) so we test the * local plugin code, not whatever is installed globally. */ -export function spawnWebappDev(args: string[], options: { cwd: string; timeout?: number }): Promise { +export function spawnUiBundleDev( + args: string[], + options: { cwd: string; timeout?: number } +): Promise { const binDev = join(process.cwd(), 'bin', 'dev.js'); const proc = spawn( process.execPath, @@ -86,7 +89,7 @@ export function spawnWebappDev(args: string[], options: { cwd: string; timeout?: } }; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const timeoutMs = options.timeout ?? SPAWN_TIMEOUT; const timeoutId = setTimeout(() => { killProcessGroup('SIGKILL'); diff --git a/test/commands/ui-bundle/helpers/webappProjectUtils.ts b/test/commands/ui-bundle/helpers/webappProjectUtils.ts index 20aca99..f52af2b 100644 --- a/test/commands/ui-bundle/helpers/webappProjectUtils.ts +++ b/test/commands/ui-bundle/helpers/webappProjectUtils.ts @@ -31,14 +31,14 @@ export const REAL_HOME = homedir(); /** * Relative path from project root to the uiBundles folder. */ -const WEBAPPS_PATH = join('force-app', 'main', 'default', UI_BUNDLES_FOLDER); +const UI_BUNDLES_PATH = join('force-app', 'main', 'default', UI_BUNDLES_FOLDER); /** * Resolve the absolute path to a UI bundle directory within a project. - * If `webAppName` is omitted, returns the uiBundles folder itself. + * If `uiBundleName` is omitted, returns the uiBundles folder itself. */ -export function webappPath(projectDir: string, webAppName?: string): string { - return webAppName ? join(projectDir, WEBAPPS_PATH, webAppName) : join(projectDir, WEBAPPS_PATH); +export function uiBundlePath(projectDir: string, uiBundleName?: string): string { + return uiBundleName ? join(projectDir, UI_BUNDLES_PATH, uiBundleName) : join(projectDir, UI_BUNDLES_PATH); } /** @@ -98,12 +98,12 @@ export function createProject(session: TestSession, name: string): string { } /** - * Run `sf project generate` then `sf ui-bundle generate --name ` inside + * Run `sf project generate` then `sf ui-bundle generate --name ` inside * the project. Returns the absolute path to the generated project root. */ -export function createProjectWithWebapp(session: TestSession, projectName: string, webAppName: string): string { +export function createProjectWithUiBundle(session: TestSession, projectName: string, uiBundleName: string): string { const projectDir = createProject(session, projectName); - execSync(`sf ui-bundle generate --name ${webAppName}`, { + execSync(`sf ui-bundle generate --name ${uiBundleName}`, { cwd: projectDir, stdio: 'pipe', env: { ...process.env, HOME: REAL_HOME, USERPROFILE: REAL_HOME }, @@ -115,7 +115,7 @@ export function createProjectWithWebapp(session: TestSession, projectName: strin * Create a project with multiple UI bundles. Used to test selection flows when * more than one UI bundle exists in a single SFDX project. */ -export function createProjectWithMultipleWebapps( +export function createProjectWithMultipleUiBundles( session: TestSession, projectName: string, webAppNames: string[] @@ -135,23 +135,23 @@ export function createProjectWithMultipleWebapps( * Create the `uiBundles/` directory (empty β€” no UI bundles inside). * Used to test "empty uiBundles folder" scenario. */ -export function createEmptyWebappsDir(projectDir: string): void { - mkdirSync(webappPath(projectDir), { recursive: true }); +export function createEmptyUiBundlesDir(projectDir: string): void { + mkdirSync(uiBundlePath(projectDir), { recursive: true }); } /** - * Create a UI bundle directory without the required `.webapplication-meta.xml`. + * Create a UI bundle directory without the required `.uibundle-meta.xml`. * Used to test "no metadata file" scenario. */ -export function createWebappDirWithoutMeta(projectDir: string, name: string): void { - mkdirSync(webappPath(projectDir, name), { recursive: true }); +export function createUiBundleDirWithoutMeta(projectDir: string, name: string): void { + mkdirSync(uiBundlePath(projectDir, name), { recursive: true }); } /** - * Overwrite the `webapplication.json` manifest for a given UI bundle. + * Overwrite the `ui-bundle.json` manifest for a given UI bundle. */ -export function writeManifest(projectDir: string, webAppName: string, manifest: Record): void { - writeFileSync(join(webappPath(projectDir, webAppName), 'webapplication.json'), JSON.stringify(manifest, null, 2)); +export function writeManifest(projectDir: string, uiBundleName: string, manifest: Record): void { + writeFileSync(join(uiBundlePath(projectDir, uiBundleName), 'ui-bundle.json'), JSON.stringify(manifest, null, 2)); } /** @@ -160,7 +160,7 @@ export function writeManifest(projectDir: string, webAppName: string, manifest: * * The script is CommonJS (.cjs) to avoid ESM/shell quoting issues. */ -export function createDevServerScript(webappDir: string, port: number): string { +export function createDevServerScript(uiBundleDir: string, port: number): string { const script = [ "const http = require('http');", 'const server = http.createServer((_, res) => {', @@ -171,7 +171,7 @@ export function createDevServerScript(webappDir: string, port: number): string { ` console.log('listening on port ${port}');`, '});', ].join('\n'); - writeFileSync(join(webappDir, 'dev-server.cjs'), script); + writeFileSync(join(uiBundleDir, 'dev-server.cjs'), script); return 'node dev-server.cjs'; } @@ -180,19 +180,19 @@ export function createDevServerScript(webappDir: string, port: number): string { * `dev.command` that starts a tiny HTTP server on `devPort`, and * `dev.url` pointing to that port. Optionally sets `dev.port` (proxy port). * - * Returns `{ projectDir, webappDir }`. + * Returns `{ projectDir, uiBundleDir }`. */ export function createProjectWithDevServer( session: TestSession, projectName: string, - webAppName: string, + uiBundleName: string, devPort: number, proxyPort?: number -): { projectDir: string; webappDir: string } { - const projectDir = createProjectWithWebapp(session, projectName, webAppName); - const webappDir = webappPath(projectDir, webAppName); +): { projectDir: string; uiBundleDir: string } { + const projectDir = createProjectWithUiBundle(session, projectName, uiBundleName); + const uiBundleDir = uiBundlePath(projectDir, uiBundleName); - const devCommand = createDevServerScript(webappDir, devPort); + const devCommand = createDevServerScript(uiBundleDir, devPort); const dev: Record = { url: `http://localhost:${devPort}`, command: devCommand, @@ -200,7 +200,7 @@ export function createProjectWithDevServer( if (proxyPort !== undefined) { dev.port = proxyPort; } - writeManifest(projectDir, webAppName, { dev }); + writeManifest(projectDir, uiBundleName, { dev }); - return { projectDir, webappDir }; + return { projectDir, uiBundleDir }; } diff --git a/test/config/ManifestWatcher.test.ts b/test/config/ManifestWatcher.test.ts index 54d72a2..34e9176 100644 --- a/test/config/ManifestWatcher.test.ts +++ b/test/config/ManifestWatcher.test.ts @@ -20,14 +20,14 @@ import { expect } from 'chai'; import { SfError } from '@salesforce/core'; import { TestContext } from '@salesforce/core/testSetup'; import { ManifestWatcher } from '../../src/config/ManifestWatcher.js'; -import type { WebAppManifest, ManifestChangeEvent } from '../../src/config/types.js'; +import type { UiBundleManifest, ManifestChangeEvent } from '../../src/config/types.js'; describe('ManifestWatcher', () => { const $$ = new TestContext(); const testDir = join(process.cwd(), '.test-manifests'); - const testManifestPath = join(testDir, 'webapplication.json'); + const testManifestPath = join(testDir, 'ui-bundle.json'); - const validManifest: WebAppManifest = { + const validManifest: UiBundleManifest = { name: 'testApp', outputDir: 'dist', dev: { @@ -69,7 +69,7 @@ describe('ManifestWatcher', () => { const watcher = new ManifestWatcher({ manifestPath: testManifestPath, watch: false }); - let readyManifest: WebAppManifest | null = null; + let readyManifest: UiBundleManifest | null = null; watcher.on('ready', (manifest) => { readyManifest = manifest; }); @@ -90,7 +90,7 @@ describe('ManifestWatcher', () => { } catch (error) { expect(error).to.be.instanceOf(SfError); expect((error as SfError).name).to.equal('ManifestNotFoundError'); - expect((error as SfError).message).to.include('webapplication.json not found'); + expect((error as SfError).message).to.include('ui-bundle.json not found'); expect((error as SfError).actions).to.exist; } @@ -125,7 +125,7 @@ describe('ManifestWatcher', () => { it('should handle read permission errors', async () => { // Create a file path that doesn't exist to simulate read error - const invalidPath = join(testDir, 'nonexistent', 'webapplication.json'); + const invalidPath = join(testDir, 'nonexistent', 'ui-bundle.json'); const watcher = new ManifestWatcher({ manifestPath: invalidPath, watch: false }); @@ -431,8 +431,8 @@ describe('ManifestWatcher', () => { }); describe('Default Options', () => { - it('should use webapplication.json in current directory by default', async () => { - const defaultPath = join(process.cwd(), 'webapplication.json'); + it('should use ui-bundle.json in current directory by default', async () => { + const defaultPath = join(process.cwd(), 'ui-bundle.json'); // Create manifest in current directory writeFileSync(defaultPath, JSON.stringify(validManifest, null, 2)); diff --git a/test/config/types.test.ts b/test/config/types.test.ts index 19bdcdc..2bbac3d 100644 --- a/test/config/types.test.ts +++ b/test/config/types.test.ts @@ -15,11 +15,11 @@ */ import { expect } from 'chai'; -import { WebAppManifest, RoutingConfig } from '../../src/config/types.js'; +import { UiBundleManifest, RoutingConfig } from '../../src/config/types.js'; describe('TypeScript Types', () => { - it('should allow valid WebAppManifest', () => { - const manifest: WebAppManifest = { + it('should allow valid UiBundleManifest', () => { + const manifest: UiBundleManifest = { name: 'testApp', outputDir: 'dist', }; @@ -28,8 +28,8 @@ describe('TypeScript Types', () => { expect(manifest.outputDir).to.equal('dist'); }); - it('should allow WebAppManifest with dev config', () => { - const manifest: WebAppManifest = { + it('should allow UiBundleManifest with dev config', () => { + const manifest: UiBundleManifest = { name: 'testApp', outputDir: 'dist', dev: { @@ -42,7 +42,7 @@ describe('TypeScript Types', () => { expect(manifest.dev?.url).to.equal('http://localhost:5173'); }); - it('should allow WebAppManifest with routing config', () => { + it('should allow UiBundleManifest with routing config', () => { const routing: RoutingConfig = { rewrites: [{ route: '/api/:id', target: 'api/handler' }], redirects: [{ route: '/old', target: '/new', statusCode: 301 }], @@ -50,7 +50,7 @@ describe('TypeScript Types', () => { fallback: 'index.html', }; - const manifest: WebAppManifest = { + const manifest: UiBundleManifest = { name: 'testApp', outputDir: 'dist', routing, diff --git a/test/config/webappDiscovery.test.ts b/test/config/webappDiscovery.test.ts index f429192..985f437 100644 --- a/test/config/webappDiscovery.test.ts +++ b/test/config/webappDiscovery.test.ts @@ -18,27 +18,27 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { expect } from 'chai'; import { SfError, SfProject } from '@salesforce/core'; -import { DEFAULT_DEV_COMMAND, discoverWebapp, UI_BUNDLES_FOLDER } from '../../src/config/webappDiscovery.js'; +import { DEFAULT_DEV_COMMAND, discoverUiBundle, UI_BUNDLES_FOLDER } from '../../src/config/webappDiscovery.js'; describe('webappDiscovery', () => { - const testDir = join(process.cwd(), '.test-webapp-discovery'); + const testDir = join(process.cwd(), '.test-uiBundle-discovery'); // Standard SFDX uiBundles path - const sfdxWebappsPath = join(testDir, 'force-app', 'main', 'default', UI_BUNDLES_FOLDER); + const sfdxUiBundlesPath = join(testDir, 'force-app', 'main', 'default', UI_BUNDLES_FOLDER); // Store original resolveProjectPath let originalResolveProjectPath: typeof SfProject.resolveProjectPath; /** - * Helper to create a valid webapp directory with required .webapplication-meta.xml + * Helper to create a valid uiBundle directory with required .uibundle-meta.xml */ - function createWebapp(webappsPath: string, name: string, manifest?: object): string { - const appPath = join(webappsPath, name); + function createUiBundle(uiBundlesPath: string, name: string, manifest?: object): string { + const appPath = join(uiBundlesPath, name); mkdirSync(appPath, { recursive: true }); - // Create required .webapplication-meta.xml file - writeFileSync(join(appPath, `${name}.webapplication-meta.xml`), ''); + // Create required .uibundle-meta.xml file + writeFileSync(join(appPath, `${name}.uibundle-meta.xml`), ''); if (manifest) { - writeFileSync(join(appPath, 'webapplication.json'), JSON.stringify(manifest)); + writeFileSync(join(appPath, 'ui-bundle.json'), JSON.stringify(manifest)); } return appPath; } @@ -51,7 +51,7 @@ describe('webappDiscovery', () => { packageDirs: Array<{ path: string; default?: boolean }> = [{ path: 'force-app', default: true }] ): void { // Create SFDX project structure - mkdirSync(sfdxWebappsPath, { recursive: true }); + mkdirSync(sfdxUiBundlesPath, { recursive: true }); writeFileSync(join(testDir, 'sfdx-project.json'), JSON.stringify({ packageDirectories: packageDirs })); // Mock SfProject.resolveProjectPath to return testDir SfProject.resolveProjectPath = async () => testDir; @@ -92,22 +92,22 @@ describe('webappDiscovery', () => { }); }); - describe('discoverWebapp', () => { - it('should throw error if no webapp found (not in SFDX project)', async () => { + describe('discoverUiBundle', () => { + it('should throw error if no uiBundle found (not in SFDX project)', async () => { mockNotInSfdxProject(); try { - await discoverWebapp(undefined, testDir); + await discoverUiBundle(undefined, testDir); expect.fail('Should have thrown an error'); } catch (error) { expect(error).to.be.instanceOf(SfError); - expect((error as SfError).name).to.equal('WebappNotFoundError'); - expect((error as SfError).message).to.include('No webapp found'); + expect((error as SfError).name).to.equal('UiBundleNotFoundError'); + expect((error as SfError).message).to.include('No uiBundle found'); } }); - it('should throw error if SFDX project has no webapplications folder', async () => { - // Create SFDX project but NOT the webapplications folder + it('should throw error if SFDX project has no uiBundles folder', async () => { + // Create SFDX project but NOT the uiBundles folder writeFileSync( join(testDir, 'sfdx-project.json'), JSON.stringify({ packageDirectories: [{ path: 'force-app', default: true }] }) @@ -115,186 +115,186 @@ describe('webappDiscovery', () => { SfProject.resolveProjectPath = async () => testDir; try { - await discoverWebapp(undefined, testDir); + await discoverUiBundle(undefined, testDir); expect.fail('Should have thrown an error'); } catch (error) { expect(error).to.be.instanceOf(SfError); - expect((error as SfError).name).to.equal('WebappNotFoundError'); + expect((error as SfError).name).to.equal('UiBundleNotFoundError'); expect((error as SfError).message).to.include('No uiBundles folder found in the SFDX project'); } }); - it('should throw error if webapplications folder exists but has no valid webapps', async () => { + it('should throw error if uiBundles folder exists but has no valid uiBundles', async () => { setupSfdxProject(); - // Create directory without .webapplication-meta.xml - mkdirSync(join(sfdxWebappsPath, 'invalid-app'), { recursive: true }); + // Create directory without .uibundle-meta.xml + mkdirSync(join(sfdxUiBundlesPath, 'invalid-app'), { recursive: true }); try { - await discoverWebapp(undefined, testDir); + await discoverUiBundle(undefined, testDir); expect.fail('Should have thrown an error'); } catch (error) { expect(error).to.be.instanceOf(SfError); - expect((error as SfError).name).to.equal('WebappNotFoundError'); - expect((error as SfError).message).to.include('no valid webapps'); + expect((error as SfError).name).to.equal('UiBundleNotFoundError'); + expect((error as SfError).message).to.include('no valid uiBundles'); } }); - it('should find webapp by name when provided', async () => { + it('should find uiBundle by name when provided', async () => { setupSfdxProject(); - createWebapp(sfdxWebappsPath, 'app-a'); - createWebapp(sfdxWebappsPath, 'app-b'); + createUiBundle(sfdxUiBundlesPath, 'app-a'); + createUiBundle(sfdxUiBundlesPath, 'app-b'); - const result = await discoverWebapp('app-b', testDir); + const result = await discoverUiBundle('app-b', testDir); - expect(result.webapp?.name).to.equal('app-b'); + expect(result.uiBundle?.name).to.equal('app-b'); expect(result.autoSelected).to.be.false; - expect(result.allWebapps).to.have.length(2); + expect(result.allUiBundles).to.have.length(2); }); - it('should throw error if named webapp not found', async () => { + it('should throw error if named uiBundle not found', async () => { setupSfdxProject(); - createWebapp(sfdxWebappsPath, 'my-app'); + createUiBundle(sfdxUiBundlesPath, 'my-app'); try { - await discoverWebapp('non-existent', testDir); + await discoverUiBundle('non-existent', testDir); expect.fail('Should have thrown an error'); } catch (error) { expect(error).to.be.instanceOf(SfError); - expect((error as SfError).name).to.equal('WebappNameNotFoundError'); - expect((error as SfError).message).to.include('No webapp found with name'); + expect((error as SfError).name).to.equal('UiBundleNameNotFoundError'); + expect((error as SfError).message).to.include('No uiBundle found with name'); expect((error as SfError).message).to.include('my-app'); } }); - it('should auto-select webapp when inside its folder', async () => { - const webappsPath = join(testDir, UI_BUNDLES_FOLDER); - mkdirSync(webappsPath, { recursive: true }); - const myAppPath = createWebapp(webappsPath, 'my-app'); - createWebapp(webappsPath, 'other-app'); + it('should auto-select uiBundle when inside its folder', async () => { + const uiBundlesPath = join(testDir, UI_BUNDLES_FOLDER); + mkdirSync(uiBundlesPath, { recursive: true }); + const myAppPath = createUiBundle(uiBundlesPath, 'my-app'); + createUiBundle(uiBundlesPath, 'other-app'); - const result = await discoverWebapp(undefined, myAppPath); + const result = await discoverUiBundle(undefined, myAppPath); - expect(result.webapp?.name).to.equal('my-app'); + expect(result.uiBundle?.name).to.equal('my-app'); expect(result.autoSelected).to.be.true; }); - it('should auto-select webapp when inside subfolder', async () => { - const webappsPath = join(testDir, UI_BUNDLES_FOLDER); - mkdirSync(webappsPath, { recursive: true }); - const myAppPath = createWebapp(webappsPath, 'my-app'); + it('should auto-select uiBundle when inside subfolder', async () => { + const uiBundlesPath = join(testDir, UI_BUNDLES_FOLDER); + mkdirSync(uiBundlesPath, { recursive: true }); + const myAppPath = createUiBundle(uiBundlesPath, 'my-app'); const srcPath = join(myAppPath, 'src'); mkdirSync(srcPath, { recursive: true }); - createWebapp(webappsPath, 'other-app'); + createUiBundle(uiBundlesPath, 'other-app'); - const result = await discoverWebapp(undefined, srcPath); + const result = await discoverUiBundle(undefined, srcPath); - expect(result.webapp?.name).to.equal('my-app'); + expect(result.uiBundle?.name).to.equal('my-app'); expect(result.autoSelected).to.be.true; }); it('should use meta.xml name (manifest.name is not used)', async () => { - const webappsPath = join(testDir, UI_BUNDLES_FOLDER); - mkdirSync(webappsPath, { recursive: true }); - const myAppPath = createWebapp(webappsPath, 'folder-name', { name: 'ManifestName' }); - createWebapp(webappsPath, 'other-app'); + const uiBundlesPath = join(testDir, UI_BUNDLES_FOLDER); + mkdirSync(uiBundlesPath, { recursive: true }); + const myAppPath = createUiBundle(uiBundlesPath, 'folder-name', { name: 'ManifestName' }); + createUiBundle(uiBundlesPath, 'other-app'); - const result = await discoverWebapp(undefined, myAppPath); + const result = await discoverUiBundle(undefined, myAppPath); - // Name comes from .webapplication-meta.xml (folder-name), not manifest.name - expect(result.webapp?.name).to.equal('folder-name'); + // Name comes from .uibundle-meta.xml (folder-name), not manifest.name + expect(result.uiBundle?.name).to.equal('folder-name'); expect(result.autoSelected).to.be.true; }); - it('should return null webapp for single webapp at project root (always prompt)', async () => { + it('should return null uiBundle for single uiBundle at project root (always prompt)', async () => { setupSfdxProject(); - createWebapp(sfdxWebappsPath, 'only-app'); + createUiBundle(sfdxUiBundlesPath, 'only-app'); - const result = await discoverWebapp(undefined, testDir); + const result = await discoverUiBundle(undefined, testDir); - // Now returns null to prompt even for single webapp (reviewer feedback) - expect(result.webapp).to.be.null; + // Now returns null to prompt even for single uiBundle (reviewer feedback) + expect(result.uiBundle).to.be.null; expect(result.autoSelected).to.be.false; - expect(result.allWebapps).to.have.length(1); + expect(result.allUiBundles).to.have.length(1); }); - it('should return null webapp for multiple webapps (selection needed)', async () => { + it('should return null uiBundle for multiple uiBundles (selection needed)', async () => { setupSfdxProject(); - createWebapp(sfdxWebappsPath, 'app-a'); - createWebapp(sfdxWebappsPath, 'app-b'); + createUiBundle(sfdxUiBundlesPath, 'app-a'); + createUiBundle(sfdxUiBundlesPath, 'app-b'); - const result = await discoverWebapp(undefined, testDir); + const result = await discoverUiBundle(undefined, testDir); - expect(result.webapp).to.be.null; + expect(result.uiBundle).to.be.null; expect(result.autoSelected).to.be.false; - expect(result.allWebapps).to.have.length(2); + expect(result.allUiBundles).to.have.length(2); }); - it('should throw error when --name conflicts with current webapp directory', async () => { - const webappsPath = join(testDir, UI_BUNDLES_FOLDER); - mkdirSync(webappsPath, { recursive: true }); - const currentAppPath = createWebapp(webappsPath, 'current-app'); - createWebapp(webappsPath, 'other-app'); + it('should throw error when --name conflicts with current uiBundle directory', async () => { + const uiBundlesPath = join(testDir, UI_BUNDLES_FOLDER); + mkdirSync(uiBundlesPath, { recursive: true }); + const currentAppPath = createUiBundle(uiBundlesPath, 'current-app'); + createUiBundle(uiBundlesPath, 'other-app'); try { // Inside current-app but specifying --name other-app - await discoverWebapp('other-app', currentAppPath); + await discoverUiBundle('other-app', currentAppPath); expect.fail('Should have thrown an error'); } catch (error) { expect(error).to.be.instanceOf(SfError); - expect((error as SfError).name).to.equal('WebappNameConflictError'); + expect((error as SfError).name).to.equal('UiBundleNameConflictError'); expect((error as SfError).message).to.include('current-app'); expect((error as SfError).message).to.include('other-app'); } }); - it('should allow --name matching current webapp directory', async () => { - const webappsPath = join(testDir, UI_BUNDLES_FOLDER); - mkdirSync(webappsPath, { recursive: true }); - const currentAppPath = createWebapp(webappsPath, 'current-app'); - createWebapp(webappsPath, 'other-app'); + it('should allow --name matching current uiBundle directory', async () => { + const uiBundlesPath = join(testDir, UI_BUNDLES_FOLDER); + mkdirSync(uiBundlesPath, { recursive: true }); + const currentAppPath = createUiBundle(uiBundlesPath, 'current-app'); + createUiBundle(uiBundlesPath, 'other-app'); // Inside current-app and specifying --name current-app (should work) - const result = await discoverWebapp('current-app', currentAppPath); + const result = await discoverUiBundle('current-app', currentAppPath); - expect(result.webapp?.name).to.equal('current-app'); + expect(result.uiBundle?.name).to.equal('current-app'); expect(result.autoSelected).to.be.false; }); - it('should recognize webapp by .webapplication-meta.xml file', async () => { + it('should recognize uiBundle by .uibundle-meta.xml file', async () => { setupSfdxProject(); - // Create directory with .webapplication-meta.xml - const validAppPath = join(sfdxWebappsPath, 'valid-app'); + // Create directory with .uibundle-meta.xml + const validAppPath = join(sfdxUiBundlesPath, 'valid-app'); mkdirSync(validAppPath, { recursive: true }); - writeFileSync(join(validAppPath, 'valid-app.webapplication-meta.xml'), ''); + writeFileSync(join(validAppPath, 'valid-app.uibundle-meta.xml'), ''); - // Create directory without .webapplication-meta.xml (should be ignored) - const invalidAppPath = join(sfdxWebappsPath, 'invalid-app'); + // Create directory without .uibundle-meta.xml (should be ignored) + const invalidAppPath = join(sfdxUiBundlesPath, 'invalid-app'); mkdirSync(invalidAppPath, { recursive: true }); - const result = await discoverWebapp(undefined, testDir); + const result = await discoverUiBundle(undefined, testDir); // Only valid-app should be discovered - expect(result.allWebapps).to.have.length(1); - expect(result.allWebapps[0].name).to.equal('valid-app'); - expect(result.allWebapps[0].hasMetaXml).to.be.true; + expect(result.allUiBundles).to.have.length(1); + expect(result.allUiBundles[0].name).to.equal('valid-app'); + expect(result.allUiBundles[0].hasMetaXml).to.be.true; }); - it('should use standalone webapp when current dir has .webapplication-meta.xml', async () => { + it('should use standalone uiBundle when current dir has .uibundle-meta.xml', async () => { mockNotInSfdxProject(); - // Create a standalone webapp directory (not in webapplications folder) + // Create a standalone uiBundle directory (not in uiBundles folder) const standaloneDir = join(testDir, 'standalone-app'); mkdirSync(standaloneDir, { recursive: true }); - writeFileSync(join(standaloneDir, 'standalone-app.webapplication-meta.xml'), ''); + writeFileSync(join(standaloneDir, 'standalone-app.uibundle-meta.xml'), ''); - const result = await discoverWebapp(undefined, standaloneDir); + const result = await discoverUiBundle(undefined, standaloneDir); - expect(result.webapp?.name).to.equal('standalone-app'); - expect(result.allWebapps).to.have.length(1); + expect(result.uiBundle?.name).to.equal('standalone-app'); + expect(result.allUiBundles).to.have.length(1); }); - it('should discover webapps from multiple package directories', async () => { + it('should discover uiBundles from multiple package directories', async () => { // Create project with two packages: force-app and packages/einstein const einsteinWebappsPath = join(testDir, 'packages', 'einstein', 'main', 'default', UI_BUNDLES_FOLDER); mkdirSync(einsteinWebappsPath, { recursive: true }); @@ -302,30 +302,30 @@ describe('webappDiscovery', () => { { path: 'force-app', default: true }, { path: 'packages/einstein', default: false }, ]); - createWebapp(sfdxWebappsPath, 'force-app-webapp'); - createWebapp(einsteinWebappsPath, 'einstein-webapp'); + createUiBundle(sfdxUiBundlesPath, 'force-app-uiBundle'); + createUiBundle(einsteinWebappsPath, 'einstein-uiBundle'); - const result = await discoverWebapp(undefined, testDir); + const result = await discoverUiBundle(undefined, testDir); - expect(result.allWebapps).to.have.length(2); - const names = result.allWebapps.map((w) => w.name).sort(); - expect(names).to.deep.equal(['einstein-webapp', 'force-app-webapp']); + expect(result.allUiBundles).to.have.length(2); + const names = result.allUiBundles.map((w) => w.name).sort(); + expect(names).to.deep.equal(['einstein-uiBundle', 'force-app-uiBundle']); }); - it('should warn and use first match when directory has multiple .webapplication-meta.xml files', async () => { + it('should warn and use first match when directory has multiple .uibundle-meta.xml files', async () => { setupSfdxProject(); - // Create webapp directory with multiple metadata files (misconfiguration) - const multiMetaPath = join(sfdxWebappsPath, 'multi-meta-app'); + // Create uiBundle directory with multiple metadata files (misconfiguration) + const multiMetaPath = join(sfdxUiBundlesPath, 'multi-meta-app'); mkdirSync(multiMetaPath, { recursive: true }); - writeFileSync(join(multiMetaPath, 'alpha.webapplication-meta.xml'), ''); - writeFileSync(join(multiMetaPath, 'beta.webapplication-meta.xml'), ''); + writeFileSync(join(multiMetaPath, 'alpha.uibundle-meta.xml'), ''); + writeFileSync(join(multiMetaPath, 'beta.uibundle-meta.xml'), ''); - const result = await discoverWebapp(undefined, testDir); + const result = await discoverUiBundle(undefined, testDir); // Discovery should succeed - uses first match (order depends on readdir) - expect(result.allWebapps).to.have.length(1); - expect(['alpha', 'beta']).to.include(result.allWebapps[0].name); + expect(result.allUiBundles).to.have.length(1); + expect(['alpha', 'beta']).to.include(result.allUiBundles[0].name); }); }); }); diff --git a/test/templates/ErrorPageRenderer.test.ts b/test/templates/ErrorPageRenderer.test.ts deleted file mode 100644 index 2807055..0000000 --- a/test/templates/ErrorPageRenderer.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2026, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { ErrorPageRenderer } from '../../src/templates/ErrorPageRenderer.js'; - -describe('ErrorPageRenderer', () => { - let renderer: ErrorPageRenderer; - - beforeEach(() => { - renderer = new ErrorPageRenderer(); - }); - - describe('Dev Server Error Pages', () => { - it('should render dev server error page', () => { - const data = { - status: 'Dev Server Unavailable', - devServerUrl: 'http://localhost:5173', - workspaceScript: 'npm run dev', - proxyUrl: 'http://localhost:4545', - orgTarget: 'myorg@example.com', - }; - - const html = renderer.render(data); - - expect(html).to.include('Dev Server Unavailable'); - expect(html).to.include('http://localhost:5173'); - expect(html).to.include('npm run dev'); - expect(html).to.include('http://localhost:4545'); - expect(html).to.include('myorg@example.com'); - }); - - it('should escape HTML in dev server error page', () => { - const data = { - status: '', - devServerUrl: 'http://localhost:5173', - workspaceScript: 'npm run dev', - proxyUrl: 'http://localhost:4545', - orgTarget: 'test@example.com', - }; - - const html = renderer.render(data); - - // The existing template doesn't escape HTML in this field - // This is acceptable as the data comes from internal sources, not user input - expect(html).to.be.a('string'); - }); - }); - - describe('Template Loading', () => { - it('should load templates successfully', () => { - expect(() => new ErrorPageRenderer()).to.not.throw(); - }); - - it('should handle gracefully if runtime template missing', () => { - // This test verifies the fallback mechanism in constructor - // The actual template should exist, but the code handles missing templates - const renderer2 = new ErrorPageRenderer(); - expect(renderer2).to.be.an.instanceof(ErrorPageRenderer); - }); - }); -}); diff --git a/yarn.lock b/yarn.lock index 58bb8c4..95cec99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1018,40 +1018,33 @@ dependencies: chalk "^4.1.0" -"@conduit-client/jwt-manager@3.7.1": - version "3.7.1" - resolved "https://registry.npmjs.org/@conduit-client/jwt-manager/-/jwt-manager-3.7.1.tgz" - integrity sha512-9x+5ck9SfGpdO991KK6PeC41YapWQNAy2Ya89x771W6r4+1dyvrLbhQtWkI5Irl3vm/5B1Ml7aqMmfLUmwK9pw== +"@conduit-client/jwt-manager@3.17.0": + version "3.17.0" + resolved "https://registry.yarnpkg.com/@conduit-client/jwt-manager/-/jwt-manager-3.17.0.tgz#76e45e76ebeb19de5e014a21aaab0cb777dcb65b" + integrity sha512-u+NgdadxOqfKy7ak7z0zCrcAL4A66mOIuekob6pjogswi8cgebvtvFRO4Mag53n6pSCmRZ4DNlXIe5eylpXo3g== dependencies: jwt-decode "~3.1.2" -"@conduit-client/salesforce-lightning-service-worker@^3.7.0": - version "3.7.1" - resolved "https://registry.npmjs.org/@conduit-client/salesforce-lightning-service-worker/-/salesforce-lightning-service-worker-3.7.1.tgz" - integrity sha512-XrlbIsqpwG8AMG/jHkOG0j3WlmG+GkLr9oTvaRkBGLqlld9gAE2QtQDdCptcG7SGFUIIDIUlckKhrC1zwKfbZQ== - dependencies: - "@conduit-client/service-fetch-network" "3.7.1" - -"@conduit-client/service-fetch-network@3.7.1": - version "3.7.1" - resolved "https://registry.npmjs.org/@conduit-client/service-fetch-network/-/service-fetch-network-3.7.1.tgz" - integrity sha512-80Bju0iTYeggVWngkuQgXFG49DOAbsGS+Wt2u25nTwDKCQBKOjjEjDXqj7HbEWgnGGayWm27qVmwG7syOUDAMw== +"@conduit-client/service-fetch-network@3.17.0": + version "3.17.0" + resolved "https://registry.yarnpkg.com/@conduit-client/service-fetch-network/-/service-fetch-network-3.17.0.tgz#1f25157a5d6c55a7b10f26b7f5d5cf5833b33c63" + integrity sha512-K+KuFWU2N4kF+k/fsS4PSOM0eGdsTGfEtAuoG+/cuRPaQz2KkW33Gyk/KZGQyF+7ikBh0kGATXX1DDF+8XFopQ== dependencies: - "@conduit-client/jwt-manager" "3.7.1" - "@conduit-client/service-retry" "3.7.1" - "@conduit-client/utils" "3.7.1" + "@conduit-client/jwt-manager" "3.17.0" + "@conduit-client/service-retry" "3.17.0" + "@conduit-client/utils" "3.17.0" -"@conduit-client/service-retry@3.7.1": - version "3.7.1" - resolved "https://registry.npmjs.org/@conduit-client/service-retry/-/service-retry-3.7.1.tgz" - integrity sha512-eVffwWEWa2HddbXdqMeFSzUEDoJoQyGD4f0hZdCfEI3+pHEBD1v5Sv6APIXgnYWEvF8dPcioo+v598Vy2gctEw== +"@conduit-client/service-retry@3.17.0": + version "3.17.0" + resolved "https://registry.yarnpkg.com/@conduit-client/service-retry/-/service-retry-3.17.0.tgz#901d25827b6a71f317a112baec28b341d2dc7d56" + integrity sha512-IrjXZUltgx7P47/cniQfvQF6H2jMlsOCOUcNGm4B1fRQpi8dyv2/Rh7VQUbIRSfYsIIJA1x/yf/BdY8Mo4Q8Jw== dependencies: - "@conduit-client/utils" "3.7.1" + "@conduit-client/utils" "3.17.0" -"@conduit-client/utils@3.7.1": - version "3.7.1" - resolved "https://registry.npmjs.org/@conduit-client/utils/-/utils-3.7.1.tgz" - integrity sha512-LBqGQJpa3lUIAmjbyU1+M4g7uk66VQTg5qvQZt/DYD4PAkWD1mELiPIo2wUyxskaT4hxYTZIRbMnS8zgRcHEvg== +"@conduit-client/utils@3.17.0": + version "3.17.0" + resolved "https://registry.yarnpkg.com/@conduit-client/utils/-/utils-3.17.0.tgz#0575499db0dd92dc31e6d2588fe71fad76b05bf5" + integrity sha512-jeq1b3I/Bxe9XHQ2CjBZcc+mkY1TxUtjuowmF8iw9A6PZYG5Sl2TMxyCtbZySHxbHMdWWc3JDJuVwc/m7do0eg== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -1758,18 +1751,19 @@ resolved "https://registry.npmjs.org/@salesforce/prettier-config/-/prettier-config-0.0.3.tgz" integrity sha512-hYOhoPTCSYMDYn+U1rlEk16PoBeAJPkrdg4/UtAzupM1mRRJOwEPMG1d7U8DxJFKuXW3DMEYWr2MwAIBDaHmFg== -"@salesforce/sdk-core@^1.29.1": - version "1.29.1" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.29.1.tgz#4679e9e2cc8c34fafb302610312f1f8eb249349e" - integrity sha512-Xc2Hh1yzV+vMj8+Ot4SjBIJgqU8OZJ51H3Hg6clKlzlzcuBzSTprHB4Cw252NWOGJ7UtG0rLGG8Q6F3qEg2SMQ== +"@salesforce/sdk-core@^1.117.0": + version "1.117.0" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.117.0.tgz#362ac95bee4d4a8f46a2bb28fe8f43b42416344d" + integrity sha512-k1/i0XAfiPpHQQtSEmJRv9x26dFMmjnWIlX3/T8sUuN0qZ3E+dnxeqGIvZ6VL3biWsZwwCD7bZ5PN5iSwEhtZg== -"@salesforce/sdk-data@^1.29.1": - version "1.29.1" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.29.1.tgz#4a1ad906d597d9e0ffca4e8c2991e2e54b90db7d" - integrity sha512-8TjrB8GiEgMEnVveQahYFd+8ak5Dr5xJgj0DoIDgIkToc1t6UzPVZAP4D/XO+aydPnzYO1ZXSPbemYOa3ar+uA== +"@salesforce/sdk-data@^1.112.7": + version "1.117.0" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.117.0.tgz#c98494741d66fb19a8d853f17264b29d9563b383" + integrity sha512-AkKPMkAxfZMLos5ozUZSE+Jvor+f4mBw4GOa1AIbXUEsdHT/FeA1MUyo++TKw2AGkgnbWFeQkSDd07KWomhaww== dependencies: - "@conduit-client/salesforce-lightning-service-worker" "^3.7.0" - "@salesforce/sdk-core" "^1.29.1" + "@conduit-client/service-fetch-network" "3.17.0" + "@conduit-client/utils" "3.17.0" + "@salesforce/sdk-core" "^1.117.0" "@salesforce/sf-plugins-core@^11.3.12": version "11.3.12" @@ -1810,16 +1804,9 @@ resolved "https://registry.npmjs.org/@salesforce/ts-types/-/ts-types-2.0.12.tgz" integrity sha512-BIJyduJC18Kc8z+arUm5AZ9VkPRyw1KKAm+Tk+9LT99eOzhNilyfKzhZ4t+tG2lIGgnJpmytZfVDZ0e2kFul8g== -"@salesforce/webapp-experimental@^1.23.0": - version "1.29.1" - resolved "https://registry.yarnpkg.com/@salesforce/webapp-experimental/-/webapp-experimental-1.29.1.tgz#98648cc166b34c1b479f9545e87d10b61dd76adc" - integrity sha512-i5ZzGs7hrjv3Fbrvmm7v8OTVoJRewWvqzXIB5JRW5l2mcz1HxK2DXriBbLHHOmYRcVSrNdN/VS8PW7YwcTuHZw== - dependencies: - "@salesforce/core" "^8.23.4" - "@salesforce/sdk-data" "^1.29.1" - axios "^1.7.7" - micromatch "^4.0.8" - path-to-regexp "^8.3.0" +"@salesforce/ui-bundle@link:../webapps/packages/webapps": + version "0.0.0" + uid "" "@shikijs/core@1.29.2": version "1.29.2" From 5dbfaf5a003cee6cfee92ad5ed3a94be24f8b371 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 11:56:13 +0530 Subject: [PATCH 03/16] docs: update SF_UI_BUNDLE_DEV_GUIDE with ui-bundle naming - Replace webui/, webapplication.json, .webapplication-meta.xml references - Replace app-based example names with bundle equivalents - Delete SF_WEBAPP_DEV_GUIDE.md (superseded by SF_UI_BUNDLE_DEV_GUIDE.md) - Fix title, debug log grep pattern, and project structure Made-with: Cursor --- SF_UI_BUNDLE_DEV_GUIDE.md | 249 +++++++-------- SF_WEBAPP_DEV_GUIDE.md | 652 -------------------------------------- 2 files changed, 119 insertions(+), 782 deletions(-) delete mode 100644 SF_WEBAPP_DEV_GUIDE.md diff --git a/SF_UI_BUNDLE_DEV_GUIDE.md b/SF_UI_BUNDLE_DEV_GUIDE.md index 8d88442..b798c4a 100644 --- a/SF_UI_BUNDLE_DEV_GUIDE.md +++ b/SF_UI_BUNDLE_DEV_GUIDE.md @@ -1,19 +1,19 @@ -# Salesforce Multi-Framework Dev Command Guide +# Salesforce UI Bundle Dev Command Guide -> **Develop web applications with seamless Salesforce integration** +> **Develop UI bundles with seamless Salesforce integration** --- ## Overview -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 webapp configuration, handles proxy routing, injects authentication headers, and supports hot reload - so you can focus on building your app. +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. ### Key Features -- **Auto-Discovery**: Automatically finds webapps in `webui/` folder -- **Optional Manifest**: `webapplication.json` is optional - uses sensible defaults -- **Auto-Selection**: Automatically selects webapp when running from inside its folder -- **Interactive Selection**: Prompts with arrow-key navigation when multiple webapps exist +- **Auto-Discovery**: Automatically finds UI bundles in `uiBundles/` folder +- **Optional Manifest**: `ui-bundle.json` is 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 @@ -24,17 +24,17 @@ The `sf ui-bundle dev` command enables local development of modern web applicati ## Quick Start -### 1. Create your webapp in the SFDX project structure +### 1. Create your UI bundle in the SFDX project structure ``` my-sfdx-project/ β”œβ”€β”€ sfdx-project.json -└── force-app/main/default/webui/ - └── my-app/ - β”œβ”€β”€ my-app.webapplication-meta.xml +└── force-app/main/default/uiBundles/ + └── my-bundle/ + β”œβ”€β”€ my-bundle.uibundle-meta.xml β”œβ”€β”€ package.json β”œβ”€β”€ src/ - └── webapplication.json + └── ui-bundle.json ``` ### 2. Run the command @@ -45,12 +45,12 @@ sf ui-bundle dev --target-org myOrg --open ### 3. Start developing -Browser opens to `http://localhost:4545` with your app running and Salesforce authentication ready. +Browser opens to `http://localhost:4545` with your UI bundle running and Salesforce authentication ready. > **Note**: > -> - `{name}.webapplication-meta.xml` is **required** to identify a valid webapp -> - `webapplication.json` is optional for dev configuration. If not present, defaults to: +> - `{name}.uibundle-meta.xml` is **required** to identify a valid UI bundle +> - `ui-bundle.json` is 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 @@ -65,25 +65,25 @@ sf ui-bundle dev [OPTIONS] ### Options -| Option | Short | Description | Default | -| -------------- | ----- | ---------------------------------- | ------------- | -| `--target-org` | `-o` | Salesforce org alias or username | Required | -| `--name` | `-n` | Web application 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 | +| 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 | ### Examples ```bash -# Simplest - auto-discovers webapplication.json +# 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 webapp by name (when multiple exist) -sf ui-bundle dev --name myApp --target-org myOrg +# 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 @@ -97,50 +97,50 @@ SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrg --- -## Webapp Discovery +## UI Bundle Discovery -The command discovers webapps using a simplified, deterministic algorithm. Webapps are identified by the presence of a `{name}.webapplication-meta.xml` file (SFDX metadata format). The optional `webapplication.json` file provides dev configuration. +The 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. ### How Discovery Works ```mermaid flowchart TD - Start["sf ui-bundle dev"] --> CheckInside{"Inside webui/
    webapp folder?"} + Start["sf ui-bundle dev"] --> CheckInside{"Inside uiBundles/
    ui bundle folder?"} CheckInside -->|Yes| HasNameInside{"--name provided?"} HasNameInside -->|Yes, different| ErrorConflict["Error: --name conflicts
    with current directory"] - HasNameInside -->|No or same| AutoSelect["Auto-select current webapp"] + HasNameInside -->|No or same| AutoSelect["Auto-select current UI bundle"] CheckInside -->|No| CheckSFDX{"In SFDX project?
    (sfdx-project.json)"} - CheckSFDX -->|Yes| CheckPath["Check force-app/main/
    default/webui/"] + CheckSFDX -->|Yes| CheckPath["Check force-app/main/
    default/uiBundles/"] CheckPath --> HasName{"--name provided?"} - CheckSFDX -->|No| CheckMetaXml{"Current dir has
    .webapplication-meta.xml?"} - CheckMetaXml -->|Yes| UseStandalone["Use current dir as webapp"] - CheckMetaXml -->|No| ErrorNone["Error: No webapp found"] + CheckSFDX -->|No| CheckMetaXml{"Current dir has
    .uibundle-meta.xml?"} + CheckMetaXml -->|Yes| UseStandalone["Use current dir as UI bundle"] + CheckMetaXml -->|No| ErrorNone["Error: No UI bundle found"] - HasName -->|Yes| SearchByName["Find webapp by name"] - HasName -->|No| Prompt["Interactive selection prompt
    (always, even if 1 webapp)"] + HasName -->|Yes| SearchByName["Find UI bundle by name"] + HasName -->|No| Prompt["Interactive selection prompt
    (always, even if 1 UI bundle)"] - SearchByName --> UseWebapp["Use webapp"] - AutoSelect --> UseWebapp - UseStandalone --> UseWebapp - Prompt --> UseWebapp + SearchByName --> UseBundle["Use UI bundle"] + AutoSelect --> UseBundle + UseStandalone --> UseBundle + Prompt --> UseBundle - UseWebapp --> StartDev["Start dev server and proxy"] + UseBundle --> StartDev["Start dev server and proxy"] ``` ### Discovery Behavior -| Scenario | Behavior | -| ----------------------------------- | --------------------------------------------------------- | -| `--name myApp` provided | Finds webapp by name, starts dev server | -| Running from inside webapp folder | Auto-selects that webapp | -| `--name` conflicts with current dir | Error: must match current webapp or run from project root | -| At SFDX project root | **Always prompts** for webapp selection | -| Outside SFDX project with meta.xml | Uses current directory as standalone webapp | -| No webapp found | Shows error with helpful message | +| 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 | ### Folder Structure (SFDX Project) @@ -148,14 +148,14 @@ flowchart TD my-sfdx-project/ β”œβ”€β”€ sfdx-project.json # SFDX project marker └── force-app/main/default/ - └── webui/ # Standard SFDX location - β”œβ”€β”€ app-one/ # Webapp 1 (with dev config) - β”‚ β”œβ”€β”€ app-one.webapplication-meta.xml # Required: identifies as webapp - β”‚ β”œβ”€β”€ webapplication.json # Optional: dev configuration + └── 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/ - └── app-two/ # Webapp 2 (no dev config) - β”œβ”€β”€ app-two.webapplication-meta.xml # Required + └── bundle-two/ # UI Bundle 2 (no dev config) + β”œβ”€β”€ bundle-two.uibundle-meta.xml # Required β”œβ”€β”€ package.json └── src/ ``` @@ -164,22 +164,22 @@ my-sfdx-project/ The command uses a simplified, deterministic approach: -1. **Inside webapp folder**: If running from `webui//` or deeper, auto-selects that webapp -2. **SFDX project root**: Uses fixed path `force-app/main/default/webui/` -3. **Standalone**: If current directory has a `.webapplication-meta.xml` file, uses it directly +1. **Inside UI bundle folder**: If running from `uiBundles//` or deeper, auto-selects that UI bundle +2. **SFDX project root**: Uses fixed path `force-app/main/default/uiBundles/` +3. **Standalone**: If current directory has a `.uibundle-meta.xml` file, uses it directly -**Important**: Only directories containing a `{name}.webapplication-meta.xml` file are recognized as valid webapps. +**Important**: Only directories containing a `{name}.uibundle-meta.xml` file are recognized as valid UI bundles. ### Interactive Selection -When multiple webapps are found, you'll see an interactive prompt: +When multiple UI bundles are found, you'll see an interactive prompt: ``` -Found 3 webapps in project -? Select the webapp to run: (Use arrow keys) -❯ app-one (webui/app-one) - app-two (webui/app-two) [no manifest] - app-three (webui/app-three) +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: @@ -245,9 +245,9 @@ The command operates in two distinct modes based on configuration: **URL precedence:** `--url` flag > `dev.url` in manifest > default `http://localhost:5173` (when command is used) -### webapplication.json Schema +### ui-bundle.json Schema -The `webapplication.json` file is **optional**. All fields are also optional - missing fields use defaults. +The `ui-bundle.json` file is **optional**. All fields are also optional - missing fields use defaults. #### Dev Configuration @@ -306,8 +306,8 @@ The `webapplication.json` file is **optional**. All fields are also optional - m ### Example: Minimal (No Manifest) ``` -webui/ -└── my-dashboard/ +uiBundles/ +└── my-bundle/ β”œβ”€β”€ package.json # Has "scripts": { "dev": "vite" } └── src/ ``` @@ -317,15 +317,15 @@ Run: `sf ui-bundle dev --target-org myOrg` Console output: ``` -Warning: No webapplication.json found for webapp "my-dashboard" - Location: my-dashboard +Warning: No ui-bundle.json found for UI bundle "my-bundle" + Location: my-bundle Using defaults: - β†’ Name: "my-dashboard" (derived from folder) + β†’ Name: "my-bundle" (derived from folder) β†’ Command: "npm run dev" β†’ Manifest watching: disabled - πŸ’‘ To customize, create a webapplication.json file in your webapp directory. + πŸ’‘ To customize, create a ui-bundle.json file in your UI bundle directory. -βœ… Using webapp: my-dashboard (webui/my-dashboard) +βœ… Using UI bundle: my-bundle (uiBundles/my-bundle) βœ… Ready for development! β†’ Proxy: http://localhost:4545 (open this in your browser) @@ -353,16 +353,16 @@ Press Ctrl+C to stop ### Manifest Hot Reload -Edit `webapplication.json` while running - changes apply automatically: +Edit `ui-bundle.json` while running - changes apply automatically: ```bash -# Console output when you change webapplication.json: +# Console output when you change ui-bundle.json: Manifest changed detected βœ“ Manifest reloaded successfully Dev server URL updated to: http://localhost:5174 ``` -> **Note**: Manifest watching is only enabled when `webapplication.json` exists. Webapps without manifests don't have this feature. +> **Note**: Manifest watching is only enabled when `ui-bundle.json` exists. UI bundles without manifests don't have this feature. ### Health Monitoring @@ -404,7 +404,7 @@ When you run the dev server yourself: ```bash # Terminal 1: Start your dev server manually -cd my-webapp +cd my-bundle npm run dev # Output: Local: http://localhost:5173/ @@ -434,58 +434,58 @@ If the URL is not reachable, the CLI starts the dev server and uses the actual U ## Troubleshooting -### "No webapp found" or "No valid webapps" +### "No UI bundle found" or "No valid UI bundles" -Ensure your webapp has the required `.webapplication-meta.xml` file: +Ensure your UI bundle has the required `.uibundle-meta.xml` file: ``` -force-app/main/default/webui/ -└── my-app/ - β”œβ”€β”€ my-app.webapplication-meta.xml # Required! +force-app/main/default/uiBundles/ +└── my-bundle/ + β”œβ”€β”€ my-bundle.uibundle-meta.xml # Required! β”œβ”€β”€ package.json - └── webapplication.json # Optional (for dev config) + └── ui-bundle.json # Optional (for dev config) ``` -The `.webapplication-meta.xml` file identifies a valid SFDX webapp. Without it, the directory is ignored. +The `.uibundle-meta.xml` file identifies a valid SFDX UI bundle. Without it, the directory is ignored. -### "You are inside webapp X but specified --name Y" +### "You are inside UI bundle X but specified --name Y" -This error occurs when you're inside one webapp folder but try to run a different webapp: +This error occurs when you're inside one UI bundle folder but try to run a different one: ```bash -# You're in FirstWebApp folder but trying to run SecondWebApp -cd webui/FirstWebApp -sf ui-bundle dev --name SecondWebApp --target-org myOrg # Error! +# 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 `--name` to use the current webapp +- Remove `--name` to use the current UI bundle - Navigate to the project root and use `--name` -- Navigate to the correct webapp folder +- Navigate to the correct UI bundle folder -### "No webapp found with name X" +### "No UI bundle found with name X" -The `--name` flag matches the folder name of the webapp. +The `--name` flag matches the folder name of the UI bundle. ```bash -# This looks for webapp named "myApp" -sf ui-bundle dev --name myApp --target-org myOrg +# This looks for UI bundle named "myBundle" +sf ui-bundle dev --name myBundle --target-org myOrg ``` ### "Dependencies Not Installed" / "command not found" -Install dependencies in your webapp folder: +Install dependencies in your UI bundle folder: ```bash -cd webui/my-app +cd uiBundles/my-bundle npm install ``` ### "No Dev Server Detected" 1. Ensure dev server is running: `npm run dev` -2. Verify URL in `webapplication.json` is correct +2. Verify URL in `ui-bundle.json` is correct 3. Try explicit URL: `sf ui-bundle dev --url http://localhost:5173 --target-org myOrg` ### "Port 4545 already in use" @@ -514,11 +514,11 @@ Enable detailed logging by setting `SF_LOG_LEVEL=debug`. Debug logs are written **Step 1: Start log tail in Terminal 1** ```bash -# Tail today's log file, filtering for webapp messages -tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev +# 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 WebappDev | jq -r '.msg' +tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered UiBundleDev | jq -r '.msg' ``` **Step 2: Run command in Terminal 2** @@ -530,9 +530,9 @@ SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrg **Example debug output:** ``` -Discovering webapplication.json manifest(s)... -Using webapp: myApp at webui/my-app -Manifest loaded: myApp +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 @@ -546,11 +546,11 @@ Proxy server running on http://localhost:4545 The command integrates with the Salesforce VSCode UI Preview extension (`salesforcedx-vscode-ui-preview`): -1. Extension detects `webapplication.json` in workspace +1. Extension detects `ui-bundle.json` in workspace 2. User clicks "Preview" button on the file 3. Extension executes: `sf ui-bundle dev --target-org --open` -4. If multiple webapps exist, uses `--name` to specify which one -5. Browser opens with the app running +4. If multiple UI bundles exist, uses `--name` to specify which one +5. Browser opens with your UI bundle running --- @@ -607,10 +607,8 @@ yarn build # Rebuild - no re-linking needed ``` plugin-ui-bundle-dev/ β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ commands/webapp/ +β”‚ β”œβ”€β”€ commands/ui-bundle/ β”‚ β”‚ └── dev.ts # Main command implementation -β”‚ β”œβ”€β”€ auth/ -β”‚ β”‚ └── org.ts # Salesforce authentication β”‚ β”œβ”€β”€ config/ β”‚ β”‚ β”œβ”€β”€ manifest.ts # Manifest type definitions β”‚ β”‚ β”œβ”€β”€ ManifestWatcher.ts # File watching and hot reload @@ -620,32 +618,23 @@ plugin-ui-bundle-dev/ β”‚ β”‚ β”œβ”€β”€ ProxyServer.ts # HTTP/WebSocket proxy server β”‚ β”‚ β”œβ”€β”€ handler.ts # Request routing and forwarding β”‚ β”‚ └── routing.ts # URL pattern matching -β”‚ β”œβ”€β”€ server/ -β”‚ β”‚ └── DevServerManager.ts # Dev server process management -β”‚ β”œβ”€β”€ error/ -β”‚ β”‚ β”œβ”€β”€ ErrorHandler.ts # Error creation utilities -β”‚ β”‚ β”œβ”€β”€ DevServerErrorParser.ts -β”‚ β”‚ └── ErrorPageRenderer.ts -β”‚ └── templates/ -β”‚ └── error-page.html # Error page template +β”‚ └── server/ +β”‚ └── DevServerManager.ts # Dev server process management β”œβ”€β”€ messages/ -β”‚ └── webapp.dev.md # CLI messages and help text +β”‚ └── ui-bundle.dev.md # CLI messages and help text └── schemas/ - └── webapp-dev.json # JSON schema for output + └── ui__bundle-dev.json # JSON schema for output ``` ### Key Components -| Component | Purpose | -| ---------------------- | ------------------------------------------------ | -| `dev.ts` | Command orchestration and lifecycle | -| `webappDiscovery.ts` | SFDX project detection and webapp discovery | -| `org.ts` | Salesforce authentication token management | -| `ProxyServer.ts` | HTTP proxy with WebSocket support | -| `handler.ts` | Request routing to dev server or Salesforce | -| `DevServerManager.ts` | Dev server process spawning and monitoring | -| `ManifestWatcher.ts` | webapplication.json file watching for hot reload | -| `ErrorPageRenderer.ts` | Browser error page generation | +| 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 | --- diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_WEBAPP_DEV_GUIDE.md deleted file mode 100644 index 98c4e23..0000000 --- a/SF_WEBAPP_DEV_GUIDE.md +++ /dev/null @@ -1,652 +0,0 @@ -# Salesforce Webapp Dev Command Guide - -> **Develop web applications with seamless Salesforce integration** - ---- - -## Overview - -The `sf webapp dev` command enables local development of modern web applications (React, Vue, Angular, etc.) with automatic Salesforce authentication. It intelligently discovers your webapp configuration, handles proxy routing, injects authentication headers, and supports hot reload - so you can focus on building your app. - -### Key Features - -- **Auto-Discovery**: Automatically finds webapps in `webapplications/` folder -- **Optional Manifest**: `webapplication.json` is optional - uses sensible defaults -- **Auto-Selection**: Automatically selects webapp when running from inside its folder -- **Interactive Selection**: Prompts with arrow-key navigation when multiple webapps 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 - ---- - -## Quick Start - -### 1. Create your webapp in the SFDX project structure - -``` -my-sfdx-project/ -β”œβ”€β”€ sfdx-project.json -└── force-app/main/default/webapplications/ - └── my-app/ - β”œβ”€β”€ my-app.webapplication-meta.xml - β”œβ”€β”€ package.json - β”œβ”€β”€ src/ - └── webapplication.json -``` - -### 2. Run the command - -```bash -sf webapp dev --target-org myOrg --open -``` - -### 3. Start developing - -Browser opens to `http://localhost:4545` with your app running and Salesforce authentication ready. - -> **Note**: -> -> - `{name}.webapplication-meta.xml` is **required** to identify a valid webapp -> - `webapplication.json` is 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 - ---- - -## Command Syntax - -```bash -sf webapp dev [OPTIONS] -``` - -### Options - -| Option | Short | Description | Default | -| -------------- | ----- | ---------------------------------- | ------------- | -| `--target-org` | `-o` | Salesforce org alias or username | Required | -| `--name` | `-n` | Web application 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 | - -### Examples - -```bash -# Simplest - auto-discovers webapplication.json -sf webapp dev --target-org myOrg - -# With browser auto-open -sf webapp dev --target-org myOrg --open - -# Specify webapp by name (when multiple exist) -sf webapp dev --name myApp --target-org myOrg - -# Custom port -sf webapp dev --target-org myOrg --port 8080 - -# Explicit dev server URL (skip auto-detection) -sf webapp dev --target-org myOrg --url http://localhost:5173 - -# Debug mode -SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg -``` - ---- - -## Webapp Discovery - -The command discovers webapps using a simplified, deterministic algorithm. Webapps are identified by the presence of a `{name}.webapplication-meta.xml` file (SFDX metadata format). The optional `webapplication.json` file provides dev configuration. - -### How Discovery Works - -```mermaid -flowchart TD - Start["sf webapp dev"] --> CheckInside{"Inside webapplications/
    webapp folder?"} - - CheckInside -->|Yes| HasNameInside{"--name provided?"} - HasNameInside -->|Yes, different| ErrorConflict["Error: --name conflicts
    with current directory"] - HasNameInside -->|No or same| AutoSelect["Auto-select current webapp"] - - CheckInside -->|No| CheckSFDX{"In SFDX project?
    (sfdx-project.json)"} - - CheckSFDX -->|Yes| CheckPath["Check force-app/main/
    default/webapplications/"] - CheckPath --> HasName{"--name provided?"} - - CheckSFDX -->|No| CheckMetaXml{"Current dir has
    .webapplication-meta.xml?"} - CheckMetaXml -->|Yes| UseStandalone["Use current dir as webapp"] - CheckMetaXml -->|No| ErrorNone["Error: No webapp found"] - - HasName -->|Yes| SearchByName["Find webapp by name"] - HasName -->|No| Prompt["Interactive selection prompt
    (always, even if 1 webapp)"] - - SearchByName --> UseWebapp["Use webapp"] - AutoSelect --> UseWebapp - UseStandalone --> UseWebapp - Prompt --> UseWebapp - - UseWebapp --> StartDev["Start dev server and proxy"] -``` - -### Discovery Behavior - -| Scenario | Behavior | -| ----------------------------------- | --------------------------------------------------------- | -| `--name myApp` provided | Finds webapp by name, starts dev server | -| Running from inside webapp folder | Auto-selects that webapp | -| `--name` conflicts with current dir | Error: must match current webapp or run from project root | -| At SFDX project root | **Always prompts** for webapp selection | -| Outside SFDX project with meta.xml | Uses current directory as standalone webapp | -| No webapp found | Shows error with helpful message | - -### Folder Structure (SFDX Project) - -``` -my-sfdx-project/ -β”œβ”€β”€ sfdx-project.json # SFDX project marker -└── force-app/main/default/ - └── webapplications/ # Standard SFDX location - β”œβ”€β”€ app-one/ # Webapp 1 (with dev config) - β”‚ β”œβ”€β”€ app-one.webapplication-meta.xml # Required: identifies as webapp - β”‚ β”œβ”€β”€ webapplication.json # Optional: dev configuration - β”‚ β”œβ”€β”€ package.json - β”‚ └── src/ - └── app-two/ # Webapp 2 (no dev config) - β”œβ”€β”€ app-two.webapplication-meta.xml # Required - β”œβ”€β”€ package.json - └── src/ -``` - -### Discovery Strategy - -The command uses a simplified, deterministic approach: - -1. **Inside webapp folder**: If running from `webapplications//` or deeper, auto-selects that webapp -2. **SFDX project root**: Uses fixed path `force-app/main/default/webapplications/` -3. **Standalone**: If current directory has a `.webapplication-meta.xml` file, uses it directly - -**Important**: Only directories containing a `{name}.webapplication-meta.xml` file are recognized as valid webapps. - -### Interactive Selection - -When multiple webapps are found, you'll see an interactive prompt: - -``` -Found 3 webapps in project -? Select the webapp to run: (Use arrow keys) -❯ app-one (webapplications/app-one) - app-two (webapplications/app-two) [no manifest] - app-three (webapplications/app-three) -``` - -Format: - -- **With manifest**: `folder-name (path)` -- **No manifest**: `folder-name (path) [no manifest]` - ---- - -## Architecture - -### Request Flow - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 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 β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### How Requests Are Handled - -**Static assets (JS, CSS, HTML, images):** - -``` -Browser β†’ Proxy β†’ Dev Server β†’ Response -``` - -**Salesforce API calls (`/services/*`):** - -``` -Browser β†’ Proxy β†’ [Auth Headers Injected] β†’ Salesforce β†’ Response -``` - ---- - -## Configuration - -### Dev Server URL Resolution - -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) - -### webapplication.json Schema - -The `webapplication.json` file is **optional**. All fields are also optional - missing fields use defaults. - -#### Dev Configuration - -| 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):** - -```json -{ - "dev": { - "command": "npm run dev" - } -} -``` - -- CLI runs `npm run dev` and 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):** - -```json -{ - "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 dev` in 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 Configuration (Optional) - -```json -{ - "routing": { - "rewrites": [{ "route": "/api/:path*", "target": "/services/apexrest/:path*" }], - "redirects": [{ "route": "/old-path", "target": "/new-path", "statusCode": 301 }], - "trailingSlash": "never", - "fallback": "/index.html" - } -} -``` - -### Example: Minimal (No Manifest) - -``` -webapplications/ -└── my-dashboard/ - β”œβ”€β”€ package.json # Has "scripts": { "dev": "vite" } - └── src/ -``` - -Run: `sf webapp dev --target-org myOrg` - -Console output: - -``` -Warning: No webapplication.json found for webapp "my-dashboard" - Location: my-dashboard - Using defaults: - β†’ Name: "my-dashboard" (derived from folder) - β†’ Command: "npm run dev" - β†’ Manifest watching: disabled - πŸ’‘ To customize, create a webapplication.json file in your webapp directory. - -βœ… Using webapp: my-dashboard (webapplications/my-dashboard) - -βœ… Ready for development! - β†’ Proxy: http://localhost:4545 (open this in your browser) - β†’ Dev server: http://localhost:5173 -Press Ctrl+C to stop -``` - -### Example: Dev + Routing - -```json -{ - "dev": { - "command": "npm run dev" - }, - "routing": { - "rewrites": [{ "route": "/api/:path*", "target": "/services/apexrest/:path*" }], - "trailingSlash": "never" - } -} -``` - ---- - -## Features - -### Manifest Hot Reload - -Edit `webapplication.json` while running - changes apply automatically: - -```bash -# Console output when you change webapplication.json: -Manifest changed detected -βœ“ Manifest reloaded successfully -Dev server URL updated to: http://localhost:5174 -``` - -> **Note**: Manifest watching is only enabled when `webapplication.json` exists. Webapps without manifests don't have this feature. - -### Health Monitoring - -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 - -### WebSocket Support - -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. - -### Code Builder Support - -Automatically detects Salesforce Code Builder environment and binds to `0.0.0.0` for proper port forwarding in cloud environments. - ---- - -## The `--url` Flag - -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 | - -### Connect to Existing Dev Server (Proxy-Only Mode) - -When you run the dev server yourself: - -```bash -# Terminal 1: Start your dev server manually -cd my-webapp -npm run dev -# Output: Local: http://localhost:5173/ - -# Terminal 2: Connect proxy to your running server -sf webapp dev --url http://localhost:5173 --target-org myOrg -``` - -**Output:** - -``` -βœ… 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) -``` - -### Override Default Port (Command Mode) - -When using `dev.command`, the default URL is `http://localhost:5173`. Override with `--url` if your dev server uses a different port: - -```bash -sf webapp dev --url http://localhost:3000 --target-org myOrg -``` - -If the URL is not reachable, the CLI starts the dev server and uses the actual URL (with a warning if it differs). - ---- - -## Troubleshooting - -### "No webapp found" or "No valid webapps" - -Ensure your webapp has the required `.webapplication-meta.xml` file: - -``` -force-app/main/default/webapplications/ -└── my-app/ - β”œβ”€β”€ my-app.webapplication-meta.xml # Required! - β”œβ”€β”€ package.json - └── webapplication.json # Optional (for dev config) -``` - -The `.webapplication-meta.xml` file identifies a valid SFDX webapp. Without it, the directory is ignored. - -### "You are inside webapp X but specified --name Y" - -This error occurs when you're inside one webapp folder but try to run a different webapp: - -```bash -# You're in FirstWebApp folder but trying to run SecondWebApp -cd webapplications/FirstWebApp -sf webapp dev --name SecondWebApp --target-org myOrg # Error! -``` - -**Solutions:** - -- Remove `--name` to use the current webapp -- Navigate to the project root and use `--name` -- Navigate to the correct webapp folder - -### "No webapp found with name X" - -The `--name` flag matches the folder name of the webapp. - -```bash -# This looks for webapp named "myApp" -sf webapp dev --name myApp --target-org myOrg -``` - -### "Dependencies Not Installed" / "command not found" - -Install dependencies in your webapp folder: - -```bash -cd webapplications/my-app -npm install -``` - -### "No Dev Server Detected" - -1. Ensure dev server is running: `npm run dev` -2. Verify URL in `webapplication.json` is correct -3. Try explicit URL: `sf webapp dev --url http://localhost:5173 --target-org myOrg` - -### "Port 4545 already in use" - -```bash -# Use a different port -sf webapp dev --port 8080 --target-org myOrg - -# Or find and kill the process using the port -lsof -i :4545 -kill -9 -``` - -### "Authentication Failed" - -Re-authorize your Salesforce org: - -```bash -sf org login web --alias myOrg -``` - -### Debug Mode - -Enable 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** - -```bash -# Tail today's log file, filtering for webapp messages -tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev - -# Or for cleaner output (requires jq): -tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev | jq -r '.msg' -``` - -**Step 2: Run command in Terminal 2** - -```bash -SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg -``` - -**Example debug output:** - -``` -Discovering webapplication.json manifest(s)... -Using webapp: myApp at webapplications/my-app -Manifest loaded: myApp -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 -``` - ---- - -## VSCode Integration - -The command integrates with the Salesforce VSCode UI Preview extension (`salesforcedx-vscode-ui-preview`): - -1. Extension detects `webapplication.json` in workspace -2. User clicks "Preview" button on the file -3. Extension executes: `sf webapp dev --target-org --open` -4. If multiple webapps exist, uses `--name` to specify which one -5. Browser opens with the app running - ---- - -## JSON Output - -For scripting and CI/CD, use the `--json` flag: - -```bash -sf webapp dev --target-org myOrg --json -``` - -Output: - -```json -{ - "status": 0, - "result": { - "url": "http://localhost:4545", - "devServerUrl": "http://localhost:5173" - } -} -``` - ---- - -## Plugin Development - -### Building the Plugin - -```bash -cd /path/to/plugin-app-dev - -# Install dependencies -yarn install - -# Build -yarn build - -# Link to SF CLI -sf plugins link . - -# Verify installation -sf plugins -``` - -### After Code Changes - -```bash -yarn build # Rebuild - no re-linking needed -``` - -### Project Structure - -``` -plugin-app-dev/ -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ commands/webapp/ -β”‚ β”‚ └── dev.ts # Main command implementation -β”‚ β”œβ”€β”€ auth/ -β”‚ β”‚ └── org.ts # Salesforce authentication -β”‚ β”œβ”€β”€ 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 -β”‚ β”œβ”€β”€ error/ -β”‚ β”‚ β”œβ”€β”€ ErrorHandler.ts # Error creation utilities -β”‚ β”‚ β”œβ”€β”€ DevServerErrorParser.ts -β”‚ β”‚ └── ErrorPageRenderer.ts -β”‚ └── templates/ -β”‚ └── error-page.html # Error page template -β”œβ”€β”€ messages/ -β”‚ └── webapp.dev.md # CLI messages and help text -└── schemas/ - └── webapp-dev.json # JSON schema for output -``` - -### Key Components - -| Component | Purpose | -| ---------------------- | ------------------------------------------------ | -| `dev.ts` | Command orchestration and lifecycle | -| `webappDiscovery.ts` | SFDX project detection and webapp discovery | -| `org.ts` | Salesforce authentication token management | -| `ProxyServer.ts` | HTTP proxy with WebSocket support | -| `handler.ts` | Request routing to dev server or Salesforce | -| `DevServerManager.ts` | Dev server process spawning and monitoring | -| `ManifestWatcher.ts` | webapplication.json file watching for hot reload | -| `ErrorPageRenderer.ts` | Browser error page generation | - ---- - -**Repository:** [github.com/salesforcecli/plugin-app-dev](https://github.com/salesforcecli/plugin-app-dev) From 58596ad65933c7a9779a1b1d53e1f202a7d81258 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 12:01:27 +0530 Subject: [PATCH 04/16] docs: replace app-based example names with bundle in messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MyApp β†’ MyBundle in flags.name.description - myUiBundle β†’ myBundle in command examples Made-with: Cursor --- messages/ui-bundle.dev.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/messages/ui-bundle.dev.md b/messages/ui-bundle.dev.md index 5e49962..9ab5212 100644 --- a/messages/ui-bundle.dev.md +++ b/messages/ui-bundle.dev.md @@ -18,7 +18,7 @@ Name of the UI bundle to preview. # flags.name.description -The unique name of the UI bundle, as defined by the "name" property in the ui-bundle.json runtime configuration file. The ui-bundle.json file is located in the "uiBundles" metadata directory of your DX project, such as force-app/main/default/uiBundles/MyApp/ui-bundle.json. +The unique name of the UI bundle, as defined by the "name" property in the ui-bundle.json runtime configuration file. The ui-bundle.json file is located in the "uiBundles" metadata directory of your DX project, such as force-app/main/default/uiBundles/MyBundle/ui-bundle.json. If you don't specify this flag, the command automatically discovers the ui-bundle.json files in the current directory and subdirectories. If the command finds only one ui-bundle.json, it automatically uses it. If it finds multiple files, the command prompts you to select one. @@ -58,11 +58,11 @@ This flag saves you from manually copying and pasting the URL. The browser opens - Start the dev server by explicitly specifying the UI bundle's name: - <%= config.bin %> <%= command.id %> --name myUiBundle --target-org myorg + <%= config.bin %> <%= command.id %> --name myBundle --target-org myorg - Start at the specified dev server URL: - <%= config.bin %> <%= command.id %> --name myUiBundle --url http://localhost:5173 --target-org myorg + <%= config.bin %> <%= command.id %> --name myBundle --url http://localhost:5173 --target-org myorg - Start with a custom proxy port and automatically open the proxy server URL in your browser: From 1e84822cdaa0b121adf63b47f01a225768e6fe83 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 13:01:22 +0530 Subject: [PATCH 05/16] fix: align moduleResolution with ui-bundle package build target Switch tsconfig from Node16 to Preserve/Bundler to match how @salesforce/ui-bundle generates its .d.ts files. The old @salesforce/webapp-experimental included .js extensions in d.ts imports (Node16-compatible), but @salesforce/ui-bundle dropped them (Bundler-style), causing ESLint type resolution failures. Also corrects UIBundleManifest import casing to match the actual export name. Made-with: Cursor --- src/config/manifest.ts | 4 ++-- test/tsconfig.json | 4 +++- tsconfig.json | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/config/manifest.ts b/src/config/manifest.ts index d3632e0..5f9fd5e 100644 --- a/src/config/manifest.ts +++ b/src/config/manifest.ts @@ -16,14 +16,14 @@ // Re-export base types from @salesforce/ui-bundle package export type { - UiBundleManifest as BaseUiBundleManifest, + UIBundleManifest as BaseUiBundleManifest, RoutingConfig, RewriteRule, RedirectRule, } from '@salesforce/ui-bundle/app'; // Import for local use -import type { UiBundleManifest as BaseUiBundleManifest } from '@salesforce/ui-bundle/app'; +import type { UIBundleManifest as BaseUiBundleManifest } from '@salesforce/ui-bundle/app'; /** * Development configuration (plugin-specific extension) diff --git a/test/tsconfig.json b/test/tsconfig.json index a5f451c..e3e01c2 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,6 +2,8 @@ "extends": "@salesforce/dev-config/tsconfig-test-strict-esm", "include": ["./**/*.ts"], "compilerOptions": { - "skipLibCheck": true + "skipLibCheck": true, + "module": "Preserve", + "moduleResolution": "Bundler" } } diff --git a/tsconfig.json b/tsconfig.json index 69e50b5..07d49ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,9 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "skipLibCheck": true + "skipLibCheck": true, + "module": "Preserve", + "moduleResolution": "Bundler" }, "include": ["./src/**/*.ts"] } From 3545a5fb57780c8f2fda05af3342beab9cc68851 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 13:06:54 +0530 Subject: [PATCH 06/16] chore: switch @salesforce/ui-bundle from local file link to npm ^1.117.2 Made-with: Cursor --- package.json | 2 +- yarn.lock | 31 +++++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index de796db..f529a96 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@salesforce/core": "^8.25.1", "@salesforce/kit": "^3.2.4", "@salesforce/sf-plugins-core": "^12.2.6", - "@salesforce/ui-bundle": "file:../webapps/packages/webapps", + "@salesforce/ui-bundle": "^1.117.2", "chokidar": "^3.6.0", "http-proxy": "^1.18.1", "micromatch": "^4.0.8", diff --git a/yarn.lock b/yarn.lock index 95cec99..2032c09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1751,19 +1751,19 @@ resolved "https://registry.npmjs.org/@salesforce/prettier-config/-/prettier-config-0.0.3.tgz" integrity sha512-hYOhoPTCSYMDYn+U1rlEk16PoBeAJPkrdg4/UtAzupM1mRRJOwEPMG1d7U8DxJFKuXW3DMEYWr2MwAIBDaHmFg== -"@salesforce/sdk-core@^1.117.0": - version "1.117.0" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.117.0.tgz#362ac95bee4d4a8f46a2bb28fe8f43b42416344d" - integrity sha512-k1/i0XAfiPpHQQtSEmJRv9x26dFMmjnWIlX3/T8sUuN0qZ3E+dnxeqGIvZ6VL3biWsZwwCD7bZ5PN5iSwEhtZg== +"@salesforce/sdk-core@^1.117.2": + version "1.117.2" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.117.2.tgz#95d136c95f8d13b692f9dffba0c921a59ca1486a" + integrity sha512-vT1K0DJM3Wk3jsgBLqCIh/zgv8GUUZJ3Ef9AqLp0F344XZOAEdDh6ierdA7VQx5bf8ApTb/V4TLt70Qu/P0zYA== -"@salesforce/sdk-data@^1.112.7": - version "1.117.0" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.117.0.tgz#c98494741d66fb19a8d853f17264b29d9563b383" - integrity sha512-AkKPMkAxfZMLos5ozUZSE+Jvor+f4mBw4GOa1AIbXUEsdHT/FeA1MUyo++TKw2AGkgnbWFeQkSDd07KWomhaww== +"@salesforce/sdk-data@^1.117.2": + version "1.117.2" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.117.2.tgz#38c860b92e54ad8618d56112742e0e330512f389" + integrity sha512-4cJKqz05ge7L0ILoBu1AdlbOBwYaSfrORrdY63q3UUBH0vLRsCav86ICox5PzNZGJMCM2TR2/1rwk7MxqZ3Xrg== dependencies: "@conduit-client/service-fetch-network" "3.17.0" "@conduit-client/utils" "3.17.0" - "@salesforce/sdk-core" "^1.117.0" + "@salesforce/sdk-core" "^1.117.2" "@salesforce/sf-plugins-core@^11.3.12": version "11.3.12" @@ -1804,9 +1804,16 @@ resolved "https://registry.npmjs.org/@salesforce/ts-types/-/ts-types-2.0.12.tgz" integrity sha512-BIJyduJC18Kc8z+arUm5AZ9VkPRyw1KKAm+Tk+9LT99eOzhNilyfKzhZ4t+tG2lIGgnJpmytZfVDZ0e2kFul8g== -"@salesforce/ui-bundle@link:../webapps/packages/webapps": - version "0.0.0" - uid "" +"@salesforce/ui-bundle@^1.117.2": + version "1.117.2" + resolved "https://registry.yarnpkg.com/@salesforce/ui-bundle/-/ui-bundle-1.117.2.tgz#88e97df0fba24e8a9d2da8adaf34f545e6525540" + integrity sha512-tYQtEdONbrISSgTU3TIrUvhc5f+/ioQA+N23LvROQKqLurWndMKQIME/g6FKxMvHmlEHsimodPgHuuvgRsgsKg== + dependencies: + "@salesforce/core" "^8.23.4" + "@salesforce/sdk-data" "^1.117.2" + axios "^1.7.7" + micromatch "^4.0.8" + path-to-regexp "^8.3.0" "@shikijs/core@1.29.2": version "1.29.2" From 204790b2d97524d9984a455213f9777633a20a49 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri <36835750+deepu-mungamuri94@users.noreply.github.com> Date: Sun, 29 Mar 2026 13:10:57 +0530 Subject: [PATCH 07/16] Update schemas/ui__bundle-dev.json Co-authored-by: Brian Buchanan <5377888+bpbuch@users.noreply.github.com> --- schemas/ui__bundle-dev.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/ui__bundle-dev.json b/schemas/ui__bundle-dev.json index 1655f0c..d73726c 100644 --- a/schemas/ui__bundle-dev.json +++ b/schemas/ui__bundle-dev.json @@ -16,7 +16,7 @@ }, "required": ["url", "devServerUrl"], "additionalProperties": false, - "description": "Command execution result What the sf ui-bundle dev command returns to the user" + "description": "Command execution result" } } } From e1d731a2e6515b87ec480798514b04948578fe33 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 13:24:01 +0530 Subject: [PATCH 08/16] docs: update COMMANDS.md and README.md to ui-bundle naming Replace all webapp/webapplication references with ui-bundle conventions, update command from sf webapp dev to sf ui-bundle dev, and align example names to use bundle-style naming (myBundle). Made-with: Cursor --- COMMANDS.md | 55 +++++++++++++++++++++++++++++++++++++---------------- README.md | 45 +++++++++++++++++++++++-------------------- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/COMMANDS.md b/COMMANDS.md index 2dde89a..d3d97d9 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -2,43 +2,66 @@ -- [`sf webapp dev`](#sf-webapp-dev) +- [`sf ui-bundle dev`](#sf-ui-bundle-dev) -## `sf webapp dev` +## `sf ui-bundle dev` -Preview a web app locally without needing to deploy +Start a local development proxy server for UI Bundle development with Salesforce authentication. ``` USAGE - $ sf webapp dev -n [--json] [--flags-dir ] [-t ] [-p ] + $ sf ui-bundle dev -o [--json] [--flags-dir ] [-n ] [-u ] [-p ] [-b] FLAGS - -n, --name= (required) Identifies the Web Application - -p, --port= [default: 5173] Port for the dev server - -t, --target= Selects which Web Application target to use for the preview (e.g., Lightning App, Site) + -b, --open Automatically open the proxy server URL in your default browser when the dev server is ready. + -n, --name= Name of the UI bundle to preview. + -o, --target-org= (required) Username or alias of the target org. Not required if the `target-org` + configuration variable is already set. + -p, --port= Local port where the proxy server listens. + -u, --url= URL where your developer server runs, such as https://localhost:5173. All UI, static, and hot + deployment requests are forwarded to this URL. GLOBAL FLAGS --flags-dir= Import flag values from a directory. --json Format output as json. DESCRIPTION - Preview a web app locally without needing to deploy + Start a local development proxy server for UI Bundle development with Salesforce authentication. - Starts a local development server for a Web Application, using the local project files. This enables rapid - development with hot reloading and immediate feedback. + This command starts a local development (dev) server so you can preview a UI bundle using the local metadata files in + your DX project. Using a local preview helps you quickly develop UI bundles, because you don't have to continually + deploy metadata to your org. + + The command also launches a local proxy server that sits between your UI bundle and Salesforce, automatically + injecting authentication headers from Salesforce CLI's stored tokens. The proxy allows your UI bundle to make + authenticated API calls to Salesforce without exposing credentials. + + Even though you're previewing the UI bundle locally and not deploying anything to an org, you're still required to + authorize and specify an org to use this command. + + Salesforce UI bundles are represented by the UiBundle metadata type. EXAMPLES - Start the development server: + Start the local development (dev) server by automatically discovering the UI bundle's ui-bundle.json file; use the + org with alias "myorg": + + $ sf ui-bundle dev --target-org myorg + + Start the dev server by explicitly specifying the UI bundle's name: + + $ sf ui-bundle dev --name myBundle --target-org myorg + + Start at the specified dev server URL: - $ sf webapp dev --name myWebApp + $ sf ui-bundle dev --name myBundle --url http://localhost:5173 --target-org myorg - Start the development server with a specific target: + Start with a custom proxy port and automatically open the proxy server URL in your browser: - $ sf webapp dev --name myWebApp --target "LightningApp" + $ sf ui-bundle dev --target-org myorg --port 4546 --open - Start the development server on a custom port: + Start with debug logging enabled by specifying the SF_LOG_LEVEL environment variable before running the command: - $ sf webapp dev --name myWebApp --port 8080 + $ SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myorg ``` diff --git a/README.md b/README.md index 0c2d284..9ee8432 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![NPM](https://img.shields.io/npm/v/@salesforce/plugin-app-dev.svg?label=@salesforce/plugin-app-dev)](https://www.npmjs.com/package/@salesforce/plugin-app-dev) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-app-dev.svg)](https://npmjs.org/package/@salesforce/plugin-app-dev) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) -# Salesforce CLI App Dev Plugin +# Salesforce CLI UI Bundle Dev Plugin -A Salesforce CLI plugin for building web applications that integrate with Salesforce. This plugin provides tools for local development, packaging, and deployment of webapps with built-in Salesforce authentication. +A Salesforce CLI plugin for building UI bundles that integrate with Salesforce. This plugin provides tools for local development of UI bundles with built-in Salesforce authentication. This plugin is bundled with the [Salesforce CLI](https://developer.salesforce.com/tools/sfdxcli). For more information on the CLI, read the [getting started guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm). @@ -12,12 +12,11 @@ We always recommend using the latest version of these commands bundled with the ## Features -- πŸ” **Local Development Proxy** - Run webapps locally with automatic Salesforce authentication +- πŸ” **Local Development Proxy** - Run UI bundles locally with automatic Salesforce authentication - 🌐 **Intelligent Request Routing** - Automatically routes requests between Salesforce APIs and dev servers - πŸ”„ **Dev Server Management** - Spawns and monitors dev servers (Vite, CRA, Next.js) -- 🎨 **Beautiful Error Handling** - HTML error pages with auto-refresh and diagnostics - πŸ’š **Health Monitoring** - Periodic health checks with status updates -- πŸ”§ **Hot Config Reload** - Detects `webapplication.json` changes automatically +- πŸ”§ **Hot Config Reload** - Detects `ui-bundle.json` changes automatically ## Quick Start @@ -33,12 +32,12 @@ We always recommend using the latest version of these commands bundled with the sf org login web --alias myorg ``` -3. **Create webapplication.json:** +3. **Create ui-bundle.json:** ```json { - "name": "myapp", - "label": "My Web App", + "name": "myBundle", + "label": "My UI Bundle", "version": "1.0.0", "apiVersion": "60.0", "outputDir": "dist", @@ -50,12 +49,12 @@ We always recommend using the latest version of these commands bundled with the 4. **Start development:** ```bash - sf webapp dev --name myapp --target-org myorg --open + sf ui-bundle dev --name myBundle --target-org myorg --open ``` ## Documentation -πŸ“š **[Complete Guide](SF_WEBAPP_DEV_GUIDE.md)** - Comprehensive documentation covering: +πŸ“š **[Complete Guide](SF_UI_BUNDLE_DEV_GUIDE.md)** - Comprehensive documentation covering: - Overview and architecture - Getting started (5-minute quick start) @@ -125,27 +124,27 @@ sf plugins ## Commands -### `sf webapp dev` +### `sf ui-bundle dev` -Start a local development proxy server for webapp development with Salesforce authentication. +Start a local development proxy server for UI Bundle development with Salesforce authentication. **Two operating modes:** -- **Command mode** (default): When `dev.command` is set in `webapplication.json` (or default `npm run dev`), the CLI starts the dev server. URL defaults to `http://localhost:5173`; override with `dev.url` or `--url` if needed. +- **Command mode** (default): When `dev.command` is set in `ui-bundle.json` (or default `npm run dev`), the CLI starts the dev server. URL defaults to `http://localhost:5173`; override with `dev.url` or `--url` if needed. - **URL-only mode**: When only `dev.url` or `--url` is provided (no command), the CLI assumes the dev server is already running and does not start it. Proxy only. ```bash USAGE - $ sf webapp dev --name --target-org [options] + $ sf ui-bundle dev --target-org [options] REQUIRED FLAGS - -n, --name= Name of the webapp (must match webapplication.json) -o, --target-org= Salesforce org to authenticate against OPTIONAL FLAGS + -n, --name= Name of the UI bundle (must match ui-bundle.json) -u, --url= Dev server URL. Command mode: override default 5173. URL-only: required (server must be running) -p, --port= Proxy server port (default: 4545) - --open Open browser automatically + -b, --open Open browser automatically DESCRIPTION Starts a local HTTP proxy that injects Salesforce authentication and routes @@ -154,20 +153,24 @@ DESCRIPTION URL-only mode, connects to an already-running dev server. EXAMPLES - Command mode (CLI starts dev server, default port 5173): + Start dev server by auto-discovering the UI bundle: - $ sf webapp dev --name myapp --target-org myorg --open + $ sf ui-bundle dev --target-org myorg --open + + Explicitly specify the UI bundle name: + + $ sf ui-bundle dev --name myBundle --target-org myorg --open URL-only mode (dev server already running): - $ sf webapp dev --name myapp --target-org myorg --url http://localhost:5173 --open + $ sf ui-bundle dev --name myBundle --target-org myorg --url http://localhost:5173 --open Custom proxy port: - $ sf webapp dev --name myapp --target-org myorg --port 8080 --open + $ sf ui-bundle dev --target-org myorg --port 8080 --open SEE ALSO - - Complete Guide: SF_WEBAPP_DEV_GUIDE.md + - Complete Guide: SF_UI_BUNDLE_DEV_GUIDE.md ``` From f396f4431170352ac4c75b4f6444ecfeff46b461 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 13:24:30 +0530 Subject: [PATCH 09/16] chore: regenerate json schema after description update Made-with: Cursor --- schemas/ui__bundle-dev.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/ui__bundle-dev.json b/schemas/ui__bundle-dev.json index d73726c..1655f0c 100644 --- a/schemas/ui__bundle-dev.json +++ b/schemas/ui__bundle-dev.json @@ -16,7 +16,7 @@ }, "required": ["url", "devServerUrl"], "additionalProperties": false, - "description": "Command execution result" + "description": "Command execution result What the sf ui-bundle dev command returns to the user" } } } From cc79fd50fd0c91c8a1dcde0d262e990a77345b83 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 13:25:49 +0530 Subject: [PATCH 10/16] docs: update package name references to plugin-ui-bundle-dev in README Made-with: Cursor --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9ee8432..3f454d4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# plugin-app-dev +# plugin-ui-bundle-dev -[![NPM](https://img.shields.io/npm/v/@salesforce/plugin-app-dev.svg?label=@salesforce/plugin-app-dev)](https://www.npmjs.com/package/@salesforce/plugin-app-dev) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-app-dev.svg)](https://npmjs.org/package/@salesforce/plugin-app-dev) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) +[![NPM](https://img.shields.io/npm/v/@salesforce/plugin-ui-bundle-dev.svg?label=@salesforce/plugin-ui-bundle-dev)](https://www.npmjs.com/package/@salesforce/plugin-ui-bundle-dev) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-ui-bundle-dev.svg)](https://npmjs.org/package/@salesforce/plugin-ui-bundle-dev) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) # Salesforce CLI UI Bundle Dev Plugin @@ -23,7 +23,7 @@ We always recommend using the latest version of these commands bundled with the 1. **Install the plugin:** ```bash - sf plugins install @salesforce/plugin-app-dev + sf plugins install @salesforce/plugin-ui-bundle-dev ``` 2. **Authenticate with Salesforce:** @@ -68,7 +68,7 @@ We always recommend using the latest version of these commands bundled with the ## Install ```bash -sf plugins install @salesforce/plugin-app-dev@x.y.z +sf plugins install @salesforce/plugin-ui-bundle-dev@x.y.z ``` ## Issues @@ -100,7 +100,7 @@ To build the plugin locally, make sure to have yarn installed and run the follow ```bash # Clone the repository -git clone git@github.com:salesforcecli/plugin-app-dev +git clone git@github.com:salesforcecli/plugin-ui-bundle-dev # Install the dependencies and compile yarn && yarn build From 0068ce1140c3ae4ff1ae6f32096817283ea49bec Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 16:38:59 +0530 Subject: [PATCH 11/16] chore: bump @salesforce/ui-bundle to ^1.117.3 Made-with: Cursor --- package.json | 2 +- yarn.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index f529a96..de24786 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@salesforce/core": "^8.25.1", "@salesforce/kit": "^3.2.4", "@salesforce/sf-plugins-core": "^12.2.6", - "@salesforce/ui-bundle": "^1.117.2", + "@salesforce/ui-bundle": "^1.117.3", "chokidar": "^3.6.0", "http-proxy": "^1.18.1", "micromatch": "^4.0.8", diff --git a/yarn.lock b/yarn.lock index 2032c09..8918748 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1751,19 +1751,19 @@ resolved "https://registry.npmjs.org/@salesforce/prettier-config/-/prettier-config-0.0.3.tgz" integrity sha512-hYOhoPTCSYMDYn+U1rlEk16PoBeAJPkrdg4/UtAzupM1mRRJOwEPMG1d7U8DxJFKuXW3DMEYWr2MwAIBDaHmFg== -"@salesforce/sdk-core@^1.117.2": - version "1.117.2" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.117.2.tgz#95d136c95f8d13b692f9dffba0c921a59ca1486a" - integrity sha512-vT1K0DJM3Wk3jsgBLqCIh/zgv8GUUZJ3Ef9AqLp0F344XZOAEdDh6ierdA7VQx5bf8ApTb/V4TLt70Qu/P0zYA== +"@salesforce/sdk-core@^1.117.3": + version "1.117.3" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.117.3.tgz#63a9d284373e017e13a462f9b6099a2c26257eeb" + integrity sha512-vz029He3j5BNaTWOAtK9Yxb3xj5CwKIZjxUG61T2cf/hCGxidrEftrDl2I0SNJaX4+t4M5QT+0ba6MOI4w8QDw== -"@salesforce/sdk-data@^1.117.2": - version "1.117.2" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.117.2.tgz#38c860b92e54ad8618d56112742e0e330512f389" - integrity sha512-4cJKqz05ge7L0ILoBu1AdlbOBwYaSfrORrdY63q3UUBH0vLRsCav86ICox5PzNZGJMCM2TR2/1rwk7MxqZ3Xrg== +"@salesforce/sdk-data@^1.117.3": + version "1.117.3" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.117.3.tgz#6effdb11787033168ce73ab9e3110ac7d9694d09" + integrity sha512-Tx/dy4Sb0toM036nutxr3j0TNKECpJZIi+FHrvO/apGd5fmyvNk9jlaAaC+IDw53vgiWrly+zwuH8EpxA9ssLw== dependencies: "@conduit-client/service-fetch-network" "3.17.0" "@conduit-client/utils" "3.17.0" - "@salesforce/sdk-core" "^1.117.2" + "@salesforce/sdk-core" "^1.117.3" "@salesforce/sf-plugins-core@^11.3.12": version "11.3.12" @@ -1804,13 +1804,13 @@ resolved "https://registry.npmjs.org/@salesforce/ts-types/-/ts-types-2.0.12.tgz" integrity sha512-BIJyduJC18Kc8z+arUm5AZ9VkPRyw1KKAm+Tk+9LT99eOzhNilyfKzhZ4t+tG2lIGgnJpmytZfVDZ0e2kFul8g== -"@salesforce/ui-bundle@^1.117.2": - version "1.117.2" - resolved "https://registry.yarnpkg.com/@salesforce/ui-bundle/-/ui-bundle-1.117.2.tgz#88e97df0fba24e8a9d2da8adaf34f545e6525540" - integrity sha512-tYQtEdONbrISSgTU3TIrUvhc5f+/ioQA+N23LvROQKqLurWndMKQIME/g6FKxMvHmlEHsimodPgHuuvgRsgsKg== +"@salesforce/ui-bundle@^1.117.3": + version "1.117.3" + resolved "https://registry.yarnpkg.com/@salesforce/ui-bundle/-/ui-bundle-1.117.3.tgz#20182e653ba3757367ce81cc0b5e04d1eae63da1" + integrity sha512-Vr52rmDV4vYDNpsGHzsZju8KnrQz/isWBZdi8WYEXqbjM8cUaw1qIb+TycbmJ1lFM/lQYsv+zUSy8z70maDCIQ== dependencies: "@salesforce/core" "^8.23.4" - "@salesforce/sdk-data" "^1.117.2" + "@salesforce/sdk-data" "^1.117.3" axios "^1.7.7" micromatch "^4.0.8" path-to-regexp "^8.3.0" From c500f884023429f363ff7b6fe267478585f75bcd Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Sun, 29 Mar 2026 17:34:15 +0530 Subject: [PATCH 12/16] fix: update Vite proxy test helper header from WebApp to UiBundle startViteProxyServer was sending X-Salesforce-WebApp-Proxy but dev.ts checks X-Salesforce-UiBundle-Proxy, causing the Vite proxy detection NUT to always fail. Made-with: Cursor --- test/commands/ui-bundle/helpers/devServerUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/commands/ui-bundle/helpers/devServerUtils.ts b/test/commands/ui-bundle/helpers/devServerUtils.ts index 3b5aac5..cc5246c 100644 --- a/test/commands/ui-bundle/helpers/devServerUtils.ts +++ b/test/commands/ui-bundle/helpers/devServerUtils.ts @@ -179,7 +179,7 @@ export function startTestHttpServer(port: number): Promise { /** * Start an HTTP server that mimics a Vite dev server with the * WebAppProxyHandler plugin active. Responds to health check requests - * (`?sfProxyHealthCheck=true`) with `X-Salesforce-WebApp-Proxy: true`. + * (`?sfProxyHealthCheck=true`) with `X-Salesforce-UiBundle-Proxy: true`. */ export function startViteProxyServer(port: number): Promise { return new Promise((resolve, reject) => { @@ -188,7 +188,7 @@ export function startViteProxyServer(port: number): Promise { if (url.searchParams.get('sfProxyHealthCheck') === 'true') { res.writeHead(200, { 'Content-Type': 'text/plain', - 'X-Salesforce-WebApp-Proxy': 'true', + 'X-Salesforce-UiBundle-Proxy': 'true', }); res.end('OK'); return; From 211001a44832b98ebc73ef5f410012fc36c2571d Mon Sep 17 00:00:00 2001 From: gary-chang Date: Mon, 30 Mar 2026 15:50:57 -0700 Subject: [PATCH 13/16] docs: expand UI bundle description in command help --- messages/ui-bundle.dev.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/messages/ui-bundle.dev.md b/messages/ui-bundle.dev.md index 9ab5212..7402a15 100644 --- a/messages/ui-bundle.dev.md +++ b/messages/ui-bundle.dev.md @@ -3,6 +3,7 @@ Preview a UI bundle locally and in real-time, without deploying it to your org. # description +A UI bundle refers to an application that runs on Salesforce Platform that uses a non-native UI framework, such as React. Salesforce provides native UI frameworks, such as Lighting Web Components (LWC), to build applications that run on the Salesforce Platform. But you can also use non-native JavaScript- or TypeScript-based UI frameworks, such as React, to build a UI experience for the Salesforce Platform and that you can launch from the App Launcher. UI bundles are defined by the UiBundle metadata type in your DX project. This command starts a local development (dev) server so you can preview a UI bundle using the local metadata files in your DX project. Using a local preview helps you quickly develop UI bundles, because you don't have to continually deploy metadata to your org. @@ -10,8 +11,6 @@ The command also launches a local proxy server that sits between your UI bundle Even though you're previewing the UI bundle locally and not deploying anything to an org, you're still required to authorize and specify an org to use this command. -Salesforce UI bundles are represented by the UiBundle metadata type. - # flags.name.summary Name of the UI bundle to preview. From 6f6f0d12e2b10c6e00a15a160a7f3e6b28ad25a3 Mon Sep 17 00:00:00 2001 From: gary-chang Date: Mon, 30 Mar 2026 16:08:51 -0700 Subject: [PATCH 14/16] fix: simplify schema description for command result Update JSDoc and schema to use concise description. --- schemas/ui__bundle-dev.json | 2 +- src/config/types.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/schemas/ui__bundle-dev.json b/schemas/ui__bundle-dev.json index 1655f0c..d73726c 100644 --- a/schemas/ui__bundle-dev.json +++ b/schemas/ui__bundle-dev.json @@ -16,7 +16,7 @@ }, "required": ["url", "devServerUrl"], "additionalProperties": false, - "description": "Command execution result What the sf ui-bundle dev command returns to the user" + "description": "Command execution result" } } } diff --git a/src/config/types.ts b/src/config/types.ts index 943126d..168df2e 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -22,7 +22,6 @@ export type { ManifestChangeEvent } from './ManifestWatcher.js'; /** * Command execution result - * What the sf ui-bundle dev command returns to the user */ export type UiBundleDevResult = { /** Proxy server URL (where user should open browser) */ From 7b07fadabf1c56e49263fe994c9503ab58af85dc Mon Sep 17 00:00:00 2001 From: gary-chang Date: Mon, 30 Mar 2026 16:50:40 -0700 Subject: [PATCH 15/16] docs: update error message examples to use camelCase naming convention --- src/config/webappDiscovery.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/webappDiscovery.ts b/src/config/webappDiscovery.ts index 8664f77..4e86ac5 100644 --- a/src/config/webappDiscovery.ts +++ b/src/config/webappDiscovery.ts @@ -479,8 +479,8 @@ export async function discoverUiBundle( 'Each uiBundle must have a {name}.uibundle-meta.xml file.\n\n' + 'Expected structure:\n' + ' uiBundles/\n' + - ' └── my-app/\n' + - ' β”œβ”€β”€ my-app.uibundle-meta.xml (required)\n' + + ' └── myDashboard/\n' + + ' β”œβ”€β”€ myDashboard.uibundle-meta.xml (required)\n' + ' └── ui-bundle.json (optional, for dev config)', 'UiBundleNotFoundError' ); @@ -490,8 +490,8 @@ export async function discoverUiBundle( 'No uiBundles folder found in the SFDX project.\n\n' + 'Create the folder structure in any package directory (e.g. force-app, packages/my-pkg):\n' + ' /main/default/uiBundles/\n' + - ' └── my-app/\n' + - ' β”œβ”€β”€ my-app.uibundle-meta.xml (required)\n' + + ' └── myDashboard/\n' + + ' β”œβ”€β”€ myDashboard.uibundle-meta.xml (required)\n' + ' └── ui-bundle.json (optional, for dev config)', 'UiBundleNotFoundError' ); From 5230a6a5f057d940167ea8d2725fe135d523fd05 Mon Sep 17 00:00:00 2001 From: gary-chang Date: Mon, 30 Mar 2026 16:58:33 -0700 Subject: [PATCH 16/16] fix: regenerate yarn.lock using yarn registry URLs --- yarn.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8918748..0a10f04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1751,19 +1751,19 @@ resolved "https://registry.npmjs.org/@salesforce/prettier-config/-/prettier-config-0.0.3.tgz" integrity sha512-hYOhoPTCSYMDYn+U1rlEk16PoBeAJPkrdg4/UtAzupM1mRRJOwEPMG1d7U8DxJFKuXW3DMEYWr2MwAIBDaHmFg== -"@salesforce/sdk-core@^1.117.3": - version "1.117.3" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.117.3.tgz#63a9d284373e017e13a462f9b6099a2c26257eeb" - integrity sha512-vz029He3j5BNaTWOAtK9Yxb3xj5CwKIZjxUG61T2cf/hCGxidrEftrDl2I0SNJaX4+t4M5QT+0ba6MOI4w8QDw== +"@salesforce/sdk-core@^1.118.0": + version "1.118.0" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.118.0.tgz#b0f4e0a8cbfa09a5c42d8b069ef9fb8408917df0" + integrity sha512-AhItSwaT418QPG6zxOfITNBipIwigqUEP2ElfZGtxmWKsSOogfLz2e02fMpVcOGnqa8KIhkTAzymtiapsKgJTg== -"@salesforce/sdk-data@^1.117.3": - version "1.117.3" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.117.3.tgz#6effdb11787033168ce73ab9e3110ac7d9694d09" - integrity sha512-Tx/dy4Sb0toM036nutxr3j0TNKECpJZIi+FHrvO/apGd5fmyvNk9jlaAaC+IDw53vgiWrly+zwuH8EpxA9ssLw== +"@salesforce/sdk-data@^1.118.0": + version "1.118.0" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.118.0.tgz#99129fd59de4ec1dacedc0934af1baf99e615d53" + integrity sha512-XV750l1Y+CqfO7YX3cQz3EMaqylStoAHewiiQk0qHUeHHpUbW1QAcSUvZpZxyOkdvifke2W6WWxqo5XcvWOM9A== dependencies: "@conduit-client/service-fetch-network" "3.17.0" "@conduit-client/utils" "3.17.0" - "@salesforce/sdk-core" "^1.117.3" + "@salesforce/sdk-core" "^1.118.0" "@salesforce/sf-plugins-core@^11.3.12": version "11.3.12" @@ -1805,12 +1805,12 @@ integrity sha512-BIJyduJC18Kc8z+arUm5AZ9VkPRyw1KKAm+Tk+9LT99eOzhNilyfKzhZ4t+tG2lIGgnJpmytZfVDZ0e2kFul8g== "@salesforce/ui-bundle@^1.117.3": - version "1.117.3" - resolved "https://registry.yarnpkg.com/@salesforce/ui-bundle/-/ui-bundle-1.117.3.tgz#20182e653ba3757367ce81cc0b5e04d1eae63da1" - integrity sha512-Vr52rmDV4vYDNpsGHzsZju8KnrQz/isWBZdi8WYEXqbjM8cUaw1qIb+TycbmJ1lFM/lQYsv+zUSy8z70maDCIQ== + version "1.118.0" + resolved "https://registry.yarnpkg.com/@salesforce/ui-bundle/-/ui-bundle-1.118.0.tgz#d006defab1efa0cd2b3a1d3bacac16ac6bfa4616" + integrity sha512-nzV7Qi2FQI7jbO8g9nfhmtgUOCHA7UuRa6PmRSOUfsy8SXUgha1j4jTpMqx0IFN9ugx770f+3BQ4/x4oyg5Tyw== dependencies: "@salesforce/core" "^8.23.4" - "@salesforce/sdk-data" "^1.117.3" + "@salesforce/sdk-data" "^1.118.0" axios "^1.7.7" micromatch "^4.0.8" path-to-regexp "^8.3.0"