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..3f454d4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# 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 +# 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,19 +12,18 @@ 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 1. **Install the plugin:** ```bash - sf plugins install @salesforce/plugin-app-dev + sf plugins install @salesforce/plugin-ui-bundle-dev ``` 2. **Authenticate with Salesforce:** @@ -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) @@ -69,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 @@ -101,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 @@ -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 ``` diff --git a/SF_UI_BUNDLE_DEV_GUIDE.md b/SF_UI_BUNDLE_DEV_GUIDE.md new file mode 100644 index 0000000..b798c4a --- /dev/null +++ b/SF_UI_BUNDLE_DEV_GUIDE.md @@ -0,0 +1,641 @@ +# Salesforce UI Bundle Dev Command Guide + +> **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 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 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 +- **Error Detection**: Displays helpful error pages with fix suggestions +- **Framework Agnostic**: Works with any web framework + +--- + +## Quick Start + +### 1. Create your UI bundle in the SFDX project structure + +``` +my-sfdx-project/ +β”œβ”€β”€ sfdx-project.json +└── force-app/main/default/uiBundles/ + └── my-bundle/ + β”œβ”€β”€ my-bundle.uibundle-meta.xml + β”œβ”€β”€ package.json + β”œβ”€β”€ src/ + └── ui-bundle.json +``` + +### 2. Run the command + +```bash +sf ui-bundle dev --target-org myOrg --open +``` + +### 3. Start developing + +Browser opens to `http://localhost:4545` with your UI bundle running and Salesforce authentication ready. + +> **Note**: +> +> - `{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 + +--- + +## Command Syntax + +```bash +sf ui-bundle dev [OPTIONS] +``` + +### Options + +| Option | Short | Description | Default | +| -------------- | ----- | -------------------------------- | ------------- | +| `--target-org` | `-o` | Salesforce org alias or username | Required | +| `--name` | `-n` | UI bundle name (folder name) | Auto-discover | +| `--url` | `-u` | Explicit dev server URL | Auto-detect | +| `--port` | `-p` | Proxy server port | 4545 | +| `--open` | `-b` | Open browser automatically | false | + +### Examples + +```bash +# Simplest - auto-discovers ui-bundle.json +sf ui-bundle dev --target-org myOrg + +# With browser auto-open +sf ui-bundle dev --target-org myOrg --open + +# Specify UI bundle by name (when multiple exist) +sf ui-bundle dev --name myBundle --target-org myOrg + +# Custom port +sf ui-bundle dev --target-org myOrg --port 8080 + +# Explicit dev server URL (skip auto-detection) +sf ui-bundle dev --target-org myOrg --url http://localhost:5173 + +# Debug mode +SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrg +``` + +--- + +## UI Bundle Discovery + +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 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 UI bundle"] + + CheckInside -->|No| CheckSFDX{"In SFDX project?
(sfdx-project.json)"} + + CheckSFDX -->|Yes| CheckPath["Check force-app/main/
default/uiBundles/"] + CheckPath --> HasName{"--name provided?"} + + 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 UI bundle by name"] + HasName -->|No| Prompt["Interactive selection prompt
(always, even if 1 UI bundle)"] + + SearchByName --> UseBundle["Use UI bundle"] + AutoSelect --> UseBundle + UseStandalone --> UseBundle + Prompt --> UseBundle + + UseBundle --> StartDev["Start dev server and proxy"] +``` + +### Discovery Behavior + +| 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) + +``` +my-sfdx-project/ +β”œβ”€β”€ sfdx-project.json # SFDX project marker +└── force-app/main/default/ + └── uiBundles/ # Standard SFDX location + β”œβ”€β”€ bundle-one/ # UI Bundle 1 (with dev config) + β”‚ β”œβ”€β”€ bundle-one.uibundle-meta.xml # Required: identifies as UI bundle + β”‚ β”œβ”€β”€ ui-bundle.json # Optional: dev configuration + β”‚ β”œβ”€β”€ package.json + β”‚ └── src/ + └── bundle-two/ # UI Bundle 2 (no dev config) + β”œβ”€β”€ bundle-two.uibundle-meta.xml # Required + β”œβ”€β”€ package.json + └── src/ +``` + +### Discovery Strategy + +The command uses a simplified, deterministic approach: + +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}.uibundle-meta.xml` file are recognized as valid UI bundles. + +### Interactive Selection + +When multiple UI bundles are found, you'll see an interactive prompt: + +``` +Found 3 UI bundles in project +? Select the UI bundle to run: (Use arrow keys) +❯ bundle-one (uiBundles/bundle-one) + bundle-two (uiBundles/bundle-two) [no manifest] + bundle-three (uiBundles/bundle-three) +``` + +Format: + +- **With manifest**: `folder-name (path)` +- **No manifest**: `folder-name (path) [no manifest]` + +--- + +## 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) + +### ui-bundle.json Schema + +The `ui-bundle.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) + +``` +uiBundles/ +└── my-bundle/ + β”œβ”€β”€ package.json # Has "scripts": { "dev": "vite" } + └── src/ +``` + +Run: `sf ui-bundle dev --target-org myOrg` + +Console output: + +``` +Warning: No ui-bundle.json found for UI bundle "my-bundle" + Location: my-bundle + Using defaults: + β†’ Name: "my-bundle" (derived from folder) + β†’ Command: "npm run dev" + β†’ Manifest watching: disabled + πŸ’‘ To customize, create a ui-bundle.json file in your UI bundle directory. + +βœ… Using UI bundle: my-bundle (uiBundles/my-bundle) + +βœ… Ready for development! + β†’ Proxy: http://localhost:4545 (open this in your browser) + β†’ Dev server: http://localhost:5173 +Press Ctrl+C to stop +``` + +### 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 `ui-bundle.json` while running - changes apply automatically: + +```bash +# 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 `ui-bundle.json` exists. UI bundles 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-bundle +npm run dev +# Output: Local: http://localhost:5173/ + +# Terminal 2: Connect proxy to your running server +sf ui-bundle dev --url http://localhost:5173 --target-org 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 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). + +--- + +## Troubleshooting + +### "No UI bundle found" or "No valid UI bundles" + +Ensure your UI bundle has the required `.uibundle-meta.xml` file: + +``` +force-app/main/default/uiBundles/ +└── my-bundle/ + β”œβ”€β”€ my-bundle.uibundle-meta.xml # Required! + β”œβ”€β”€ package.json + └── ui-bundle.json # Optional (for dev config) +``` + +The `.uibundle-meta.xml` file identifies a valid SFDX UI bundle. Without it, the directory is ignored. + +### "You are inside UI bundle X but specified --name Y" + +This error occurs when you're inside one UI bundle folder but try to run a different one: + +```bash +# 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 UI bundle +- Navigate to the project root and use `--name` +- Navigate to the correct UI bundle folder + +### "No UI bundle found with name X" + +The `--name` flag matches the folder name of the UI bundle. + +```bash +# 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 UI bundle folder: + +```bash +cd uiBundles/my-bundle +npm install +``` + +### "No Dev Server Detected" + +1. Ensure dev server is running: `npm run dev` +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" + +```bash +# Use a different port +sf ui-bundle dev --port 8080 --target-org myOrg + +# Or find and kill the process using the port +lsof -i :4545 +kill -9 +``` + +### "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 UiBundleDev messages +tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered UiBundleDev + +# Or for cleaner output (requires jq): +tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered UiBundleDev | jq -r '.msg' +``` + +**Step 2: Run command in Terminal 2** + +```bash +SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrg +``` + +**Example debug output:** + +``` +Discovering ui-bundle.json manifest(s)... +Using UI bundle: myBundle at uiBundles/my-bundle +Manifest loaded: myBundle +Starting dev server with command: npm run dev +Dev server ready at: http://localhost:5173/ +Using authentication for org: user@example.com +Starting proxy server on port 4545... +Proxy server running on http://localhost:4545 +``` + +--- + +## VSCode Integration + +The command integrates with the Salesforce VSCode UI Preview extension (`salesforcedx-vscode-ui-preview`): + +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 UI bundles exist, uses `--name` to specify which one +5. Browser opens with your UI bundle running + +--- + +## JSON Output + +For scripting and CI/CD, use the `--json` flag: + +```bash +sf ui-bundle 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-ui-bundle-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-ui-bundle-dev/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ commands/ui-bundle/ +β”‚ β”‚ └── dev.ts # Main command implementation +β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ β”œβ”€β”€ manifest.ts # Manifest type definitions +β”‚ β”‚ β”œβ”€β”€ ManifestWatcher.ts # File watching and hot reload +β”‚ β”‚ β”œβ”€β”€ webappDiscovery.ts # Auto-discovery logic +β”‚ β”‚ └── types.ts # Shared TypeScript types +β”‚ β”œβ”€β”€ proxy/ +β”‚ β”‚ β”œβ”€β”€ ProxyServer.ts # HTTP/WebSocket proxy server +β”‚ β”‚ β”œβ”€β”€ handler.ts # Request routing and forwarding +β”‚ β”‚ └── routing.ts # URL pattern matching +β”‚ └── server/ +β”‚ └── DevServerManager.ts # Dev server process management +β”œβ”€β”€ messages/ +β”‚ └── ui-bundle.dev.md # CLI messages and help text +└── schemas/ + └── ui__bundle-dev.json # JSON schema for output +``` + +### Key Components + +| Component | Purpose | +| --------------------- | ---------------------------------------------- | +| `dev.ts` | Command orchestration and lifecycle | +| `webappDiscovery.ts` | SFDX project detection and UI bundle discovery | +| `ProxyServer.ts` | HTTP proxy with WebSocket support | +| `DevServerManager.ts` | Dev server process spawning and monitoring | +| `ManifestWatcher.ts` | ui-bundle.json file watching for hot reload | + +--- + +**Repository:** [github.com/salesforcecli/plugin-ui-bundle-dev](https://github.com/salesforcecli/plugin-ui-bundle-dev) diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_WEBAPP_DEV_GUIDE.md deleted file mode 100644 index 4095bce..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) 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..7402a15 --- /dev/null +++ b/messages/ui-bundle.dev.md @@ -0,0 +1,246 @@ +# summary + +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. + +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. + +# flags.name.summary + +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/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. + +# 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 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 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). + +# 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 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 UI bundle's name: + + <%= config.bin %> <%= command.id %> --name myBundle --target-org myorg + +- Start at the specified dev server URL: + + <%= 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: + + <%= 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 ui-bundle.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-uiBundles-found + +Found %s UI bundles in project. + +# info.uiBundle-auto-selected + +Auto-selected UI bundle "%s" (running from inside its folder). + +# info.using-uiBundle + +βœ… Using UI bundle: %s (%s). + +# info.starting-uiBundle + +βœ… Starting %s. + +# prompt.select-uiBundle + +Select the UI bundle to run: + +# info.no-manifest-defaults + +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 ui-bundle.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 UI bundle 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..de24786 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", @@ -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": "^1.117.3", "chokidar": "^3.6.0", "http-proxy": "^1.18.1", "micromatch": "^4.0.8", @@ -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..d73726c 100644 --- a/schemas/webapp-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": { @@ -16,7 +16,7 @@ }, "required": ["url", "devServerUrl"], "additionalProperties": false, - "description": "Command execution result What the sf webapp dev command returns to the user" + "description": "Command execution result" } } } diff --git a/src/commands/webapp/dev.ts b/src/commands/ui-bundle/dev.ts similarity index 85% rename from src/commands/webapp/dev.ts rename to src/commands/ui-bundle/dev.ts index 2cc476b..3a97d2c 100644 --- a/src/commands/webapp/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-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'); @@ -74,30 +74,30 @@ export default class WebappDev 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 promptWebappSelection(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,18 +133,18 @@ 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); } /** - * 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 WebappDev 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,49 +168,49 @@ export default class WebappDev extends SfCommand { } // eslint-disable-next-line complexity - public async run(): Promise { - const { flags } = await this.parse(WebappDev); + public async run(): Promise { + 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; + 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 WebappDev.promptWebappSelection(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 WebappDev 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,24 +266,24 @@ export default class WebappDev 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' ); } // 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])); @@ -303,12 +303,12 @@ export default class WebappDev 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 WebappDev extends SfCommand { this.devServerManager = new DevServerManager({ command: devCommand, url: resolvedUrl, - cwd: webappDir, + cwd: uiBundleDir, startupTimeout: 60_000, }); @@ -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 = @@ -372,7 +372,7 @@ export default class WebappDev 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 WebappDev 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...'); - const viteProxyActive = await WebappDev.checkViteProxyActive(devServerUrl); + 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; @@ -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/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..5f9fd5e 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 77ac97b..168df2e 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -15,16 +15,15 @@ */ // 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'; /** * Command execution result - * What the sf webapp 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 88d3ef9..4e86ac5 100644 --- a/src/config/webappDiscovery.ts +++ b/src/config/webappDiscovery.ts @@ -17,48 +17,48 @@ 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'; /** - * 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 + * 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; }; /** - * 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,22 +70,22 @@ 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; +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; } @@ -174,21 +174,21 @@ 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 { +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; }) ); @@ -199,53 +199,53 @@ 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: + * This handles cases where the user runs the command from inside a uiBundle 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: { uiBundlesFolder: "/project/.../uiBundles", currentUiBundleName: "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: { uiBundlesFolder: "/project/.../uiBundles", currentUiBundleName: "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: { uiBundlesFolder: "/project/.../uiBundles", currentUiBundleName: 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 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; 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 - if (isWebapplicationsFolder(dirName)) { + // Case: Current directory IS the uiBundles folder + // e.g., cwd = /project/uiBundles + if (isUiBundlesFolder(dirName)) { return { - webappsFolder: currentDir, - currentWebappName: childDir ? basename(childDir) : null, + uiBundlesFolder: currentDir, + currentUiBundleName: childDir ? basename(childDir) : null, }; } - // Case: Parent directory is the webapplications folder - // e.g., cwd = /project/webapplications/my-app (parent is webapplications) - if (isWebapplicationsFolder(basename(parentDir))) { + // Case: Parent directory is the uiBundles folder + // e.g., cwd = /project/uiBundles/my-app (parent is webui) + 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 }; } @@ -260,58 +260,58 @@ function findWebapplicationsFolderUpward( depth++; } - // Not inside a webapplications folder + // Not inside a uiBundles folder return null; } /** - * Discover all webapps inside the webapplications 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 webapplications 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,234 +319,241 @@ 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 webapplications 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 webapplications/ directory (upward search) - * 2. Check for SFDX project and search webapplications in all package directories - * 3. If neither, check if current directory is a webapp (has .webapplication-meta.xml) + * 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 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 webapplications folder (upward search) - // This handles: running from webapplications/ or webapplications//src/ - const upwardResult = findWebapplicationsFolderUpward(cwd); + // Step 1: Check if we're inside a uiBundles folder (upward search) + // 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 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) { 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 webapplications 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 webapplications in all package directories - * - Webapps identified by {name}.webapplication-meta.xml - * - Always prompt for selection (even if only 1 webapp) + * 1. SFDX Project Root: Search uiBundles in all package directories + * - Webapps identified by {name}.uibundle-meta.xml + * - Always prompt for selection (even if only 1 uiBundle) * - * 2. Inside webapplications/ 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 "webapplications" 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' + - ' webapplications/\n' + - ' └── my-app/\n' + - ' β”œβ”€β”€ my-app.webapplication-meta.xml (required)\n' + - ' └── webapplication.json (optional, for dev config)', - 'WebappNotFoundError' + ' uiBundles/\n' + + ' └── myDashboard/\n' + + ' β”œβ”€β”€ myDashboard.uibundle-meta.xml (required)\n' + + ' └── ui-bundle.json (optional, for dev config)', + 'UiBundleNotFoundError' ); } 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' + - ' └── my-app/\n' + - ' β”œβ”€β”€ my-app.webapplication-meta.xml (required)\n' + - ' └── webapplication.json (optional, for dev config)', - 'WebappNotFoundError' + ' /main/default/uiBundles/\n' + + ' └── myDashboard/\n' + + ' β”œβ”€β”€ myDashboard.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/webapplications/\n' + - '2. Run from inside a webapplications// 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 3afd3d3..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 @@ -120,24 +92,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 +123,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 +148,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 +160,8 @@ export class ProxyServer extends EventEmitter { res.writeHead(statusCode, headers); res.end(body, actualCb); } - } - } + }, + }, }) as ServerResponse; return wrapped; @@ -383,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'); @@ -503,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', }; @@ -514,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/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 58% rename from test/commands/webapp/dev.nut.ts rename to test/commands/ui-bundle/dev.nut.ts index b1cc3eb..ffcab42 100644 --- a/test/commands/webapp/dev.nut.ts +++ b/test/commands/ui-bundle/dev.nut.ts @@ -21,14 +21,15 @@ 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, } 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, }); @@ -64,13 +65,13 @@ describe('webapp 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. * * * * 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,79 +93,83 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { // ── Discovery errors ────────────────────────────────────────── - // Project has no webapplications 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(`webapp dev --target-org ${targetOrg} --json`, { + 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(`webapp dev --name NonExistent --target-org ${targetOrg} --json`, { + 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'); - execSync('sf webapp generate --name appB', { cwd: projectDir, stdio: 'pipe' }); + 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(`webapp dev --name appB --target-org ${targetOrg} --json`, { + 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'); }); - // webapplications/ folder exists but is empty β†’ WebappNotFoundError. - it('should error when webapplications folder is empty', () => { - const projectDir = createProject(session, 'emptyWebapps'); - createEmptyWebappsDir(projectDir); + // uiBundles/ folder exists but is empty β†’ UiBundleNotFoundError. + it('should error when uiBundles folder is empty', () => { + const projectDir = createProject(session, 'emptyUiBundles'); + createEmptyUiBundlesDir(projectDir); - const result = execCmd(`webapp dev --target-org ${targetOrg} --json`, { + 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'); }); - // webapplications/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(`webapp dev --target-org ${targetOrg} --json`, { + 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' }, @@ -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, }); @@ -181,9 +186,9 @@ describe('webapp 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' }, @@ -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,22 +207,22 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { // ── Auto-selection ──────────────────────────────────────────── - // When cwd is inside webapplications/myApp/, discovery auto-selects that - // webapp without --name. The command proceeds past discovery and fails at + // When cwd is inside uiBundles/myApp/, discovery auto-selects that + // 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(`webapp dev --target-org ${targetOrg} --json`, { + const result = execCmd(`ui-bundle dev --target-org ${targetOrg} --json`, { ensureExitCode: 1, cwd: cwdInsideApp, }); @@ -225,11 +230,11 @@ 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 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' }, @@ -238,9 +243,9 @@ describe('webapp dev NUTs β€” Tier 2 CLI validation', () => { dev: { url: 'http://localhost:5185' }, }); - const cwdInsideAppA = webappPath(projectDir, 'appA'); + const cwdInsideAppA = uiBundlePath(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, }); @@ -253,9 +258,9 @@ describe('webapp 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(`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, }); @@ -266,13 +271,13 @@ describe('webapp 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' }, }); - 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, }); @@ -282,23 +287,20 @@ describe('webapp 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' }, }); - 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 90% rename from test/commands/webapp/dev.test.ts rename to test/commands/ui-bundle/dev.test.ts index ceb9668..626f53e 100644 --- a/test/commands/webapp/dev.test.ts +++ b/test/commands/ui-bundle/dev.test.ts @@ -17,9 +17,9 @@ 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('webapp:dev command integration', () => { +describe('ui-bundle:dev command integration', () => { const $$ = new TestContext(); afterEach(() => { @@ -49,15 +49,15 @@ describe('webapp: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('webapp: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('webapp: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('webapp: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('webapp: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('webapp: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('webapp: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('webapp: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('webapp: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('webapp: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/webapp/devPort.nut.ts b/test/commands/ui-bundle/devPort.nut.ts similarity index 89% rename from test/commands/webapp/devPort.nut.ts rename to test/commands/ui-bundle/devPort.nut.ts index 5cbff70..c4fa667 100644 --- a/test/commands/webapp/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'; /* ------------------------------------------------------------------ * @@ -42,13 +42,13 @@ 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; 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('webapp 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('webapp 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('webapp 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('webapp 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('webapp 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/webapp/devWithUrl.nut.ts b/test/commands/ui-bundle/devWithUrl.nut.ts similarity index 84% rename from test/commands/webapp/devWithUrl.nut.ts rename to test/commands/ui-bundle/devWithUrl.nut.ts index 5c6d0a6..b976b76 100644 --- a/test/commands/webapp/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'; /* ------------------------------------------------------------------ * @@ -50,12 +50,12 @@ 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; 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('webapp 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('webapp 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('webapp 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('webapp 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('webapp 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('webapp 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('webapp 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,14 +233,18 @@ describe('webapp 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', - '--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 } ); @@ -254,7 +258,7 @@ describe('webapp 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', () => { @@ -263,12 +267,12 @@ describe('webapp 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, }); @@ -280,12 +284,12 @@ describe('webapp 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, }); @@ -297,17 +301,17 @@ describe('webapp 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/webapp/helpers/devServerUtils.ts b/test/commands/ui-bundle/helpers/devServerUtils.ts similarity index 89% rename from test/commands/webapp/helpers/devServerUtils.ts rename to test/commands/ui-bundle/helpers/devServerUtils.ts index ce161cf..cc5246c 100644 --- a/test/commands/webapp/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 */ @@ -50,16 +50,19 @@ 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. */ -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, - ['--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'], @@ -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'); @@ -140,7 +143,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}`)); } }); }); @@ -176,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) => { @@ -185,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; diff --git a/test/commands/webapp/helpers/webappProjectUtils.ts b/test/commands/ui-bundle/helpers/webappProjectUtils.ts similarity index 58% rename from test/commands/webapp/helpers/webappProjectUtils.ts rename to test/commands/ui-bundle/helpers/webappProjectUtils.ts index dee8dfa..f52af2b 100644 --- a/test/commands/webapp/helpers/webappProjectUtils.ts +++ b/test/commands/ui-bundle/helpers/webappProjectUtils.ts @@ -16,22 +16,29 @@ 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. */ -export function webappPath(projectDir: string, webAppName?: string): string { - return webAppName ? join(projectDir, WEBAPPS_PATH, webAppName) : join(projectDir, WEBAPPS_PATH); +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 `uiBundleName` is omitted, returns the uiBundles folder itself. + */ +export function uiBundlePath(projectDir: string, uiBundleName?: string): string { + return uiBundleName ? join(projectDir, UI_BUNDLES_PATH, uiBundleName) : join(projectDir, UI_BUNDLES_PATH); } /** @@ -91,67 +98,69 @@ 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 { +export function createProjectWithUiBundle(session: TestSession, projectName: string, uiBundleName: string): string { const projectDir = createProject(session, projectName); - execSync(`sf webapp generate --name ${webAppName}`, { + execSync(`sf ui-bundle generate --name ${uiBundleName}`, { 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( +export function createProjectWithMultipleUiBundles( session: TestSession, projectName: string, webAppNames: string[] ): 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 }); +export function createEmptyUiBundlesDir(projectDir: string): void { + mkdirSync(uiBundlePath(projectDir), { recursive: true }); } /** - * Create a webapp 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 webapp. + * 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)); } /** - * 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. */ -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) => {', @@ -162,28 +171,28 @@ 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'; } /** - * 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). * - * 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, @@ -191,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 bc9d0a0..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 } 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 webapplications path - const sfdxWebappsPath = join(testDir, 'force-app', 'main', 'default', 'webapplications'); + // Standard SFDX uiBundles path + 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,217 +115,217 @@ 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).message).to.include('No webapplications folder found in the SFDX project'); + 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, 'webapplications'); - 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, 'webapplications'); - 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, 'webapplications'); - 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, 'webapplications'); - 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, 'webapplications'); - 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', 'webapplications'); + const einsteinWebappsPath = join(testDir, 'packages', 'einstein', 'main', 'default', UI_BUNDLES_FOLDER); mkdirSync(einsteinWebappsPath, { recursive: true }); setupSfdxProject([ { 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/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"] } diff --git a/yarn.lock b/yarn.lock index 58bb8c4..0a10f04 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.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.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.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/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.118.0" "@salesforce/sf-plugins-core@^11.3.12": version "11.3.12" @@ -1810,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/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== +"@salesforce/ui-bundle@^1.117.3": + 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.29.1" + "@salesforce/sdk-data" "^1.118.0" axios "^1.7.7" micromatch "^4.0.8" path-to-regexp "^8.3.0"