From a8579b1aec7bedb83416ef7b6fe04da2b111207f Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 6 Feb 2026 22:14:27 +0530 Subject: [PATCH 01/15] refactor(dev): improve webapp discovery and console output @W-21193297@ - Add SFDX project detection for webapp discovery - Simplify webapplications folder detection logic - Condense warning messages, remove verbose JSON examples - Add help tip for configuration options --- .prettierignore | 2 + SF_WEBAPP_DEV_GUIDE.md | 211 +++++++++++----- messages/webapp.dev.md | 45 ++-- src/commands/webapp/dev.ts | 200 +++++++++------ src/config/webappDiscovery.ts | 368 +++++++++++++++++----------- src/error/DevServerErrorParser.ts | 30 +-- src/server/DevServerManager.ts | 5 +- test/config/webappDiscovery.test.ts | 193 ++++++++++++--- 8 files changed, 677 insertions(+), 377 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..fb2179a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# CLI message templates - preserve formatting +messages/*.md diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_WEBAPP_DEV_GUIDE.md index 98268ff..1459476 100644 --- a/SF_WEBAPP_DEV_GUIDE.md +++ b/SF_WEBAPP_DEV_GUIDE.md @@ -24,15 +24,17 @@ The `sf webapp dev` command enables local development of modern web applications ## Quick Start -### 1. Create your webapp in the `webapplications/` folder +### 1. Create your webapp in the SFDX project structure ``` -my-project/ -└── webapplications/ - └── my-app/ # Your webapp folder +my-sfdx-project/ +├── sfdx-project.json +└── force-app/main/default/webapplications/ + └── my-app/ # Your webapp folder + ├── my-app.webapplication-meta.xml # Required: identifies as webapp ├── package.json ├── src/ - └── webapplication.json # Optional! + └── webapplication.json # Optional: dev configuration ``` ### 2. Run the command @@ -45,11 +47,13 @@ sf webapp dev --target-org myOrg --open Browser opens to `http://localhost:4545` with your app running and Salesforce authentication ready. -> **Note**: `webapplication.json` is optional! If not present, the command uses: +> **Note**: > -> - **Name**: Folder name (e.g., "my-app") -> - **Dev command**: `npm run dev` -> - **Manifest watching**: Disabled +> - `{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 --- @@ -95,78 +99,76 @@ SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg ## Webapp Discovery -The command automatically discovers webapps in the `webapplications/` folder. Each subfolder is treated as a webapp, with `webapplication.json` being optional. +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"] --> FindFolder["Find webapplications/ folder"] - FindFolder --> Found{"Found?"} - Found -->|No| ErrorNone["Error: No webapplications folder found"] - Found -->|Yes| HasName{"--name provided?"} + Start["sf webapp dev"] --> CheckInside{"Inside webapplications/
webapp folder?"} - HasName -->|Yes| SearchByName["Find webapp by name"] - HasName -->|No| InsideWebapp{"Running from inside a webapp?"} + 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?"} - InsideWebapp -->|Yes| AutoSelect["Auto-select current webapp"] - InsideWebapp -->|No| Count{"How many webapps?"} + CheckSFDX -->|No| CheckMetaXml{"Current dir has
.webapplication-meta.xml?"} + CheckMetaXml -->|Yes| UseStandalone["Use current dir as webapp"] + CheckMetaXml -->|No| ErrorNone["Error: No webapp found"] - Count -->|1| AutoSelectSingle["Auto-select single webapp"] - Count -->|Multiple| Prompt["Interactive selection prompt"] + HasName -->|Yes| SearchByName["Find webapp by name"] + HasName -->|No| Prompt["Interactive selection prompt
(always, even if 1 webapp)"] SearchByName --> UseWebapp["Use webapp"] AutoSelect --> UseWebapp - AutoSelectSingle --> UseWebapp + UseStandalone --> UseWebapp Prompt --> UseWebapp - UseWebapp --> HasManifest{"Has webapplication.json?"} - HasManifest -->|Yes| UseManifest["Use manifest config"] - HasManifest -->|No| UseDefaults["Use defaults (npm run dev)"] - - UseManifest --> StartDev["Start dev server and proxy"] - UseDefaults --> StartDev + UseWebapp --> StartDev["Start dev server and proxy"] ``` ### Discovery Behavior -| Scenario | Behavior | -| --------------------------------- | ---------------------------------------------- | -| `--name myApp` provided | Finds webapp by name (manifest name or folder) | -| Running from inside webapp folder | Auto-selects that webapp | -| Single webapp found | Auto-selects it | -| Multiple webapps found | Shows interactive selection with arrow keys | -| No webapplications folder | Shows error with helpful message | +| 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 +### Folder Structure (SFDX Project) ``` -my-project/ -└── webapplications/ # Required folder (case-insensitive) - ├── app-one/ # Webapp 1 (with manifest) - │ ├── webapplication.json - │ ├── package.json - │ └── src/ - ├── app-two/ # Webapp 2 (no manifest - uses defaults) - │ ├── package.json - │ └── src/ - └── app-three/ # Webapp 3 (partial manifest) - ├── webapplication.json # Only has dev.command - └── src/ +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/ ``` -### Search Scope - -The command searches for the `webapplications/` folder: +### Discovery Strategy -1. **Upward**: First checks if you're inside a webapplications folder -2. **Downward**: Then searches child directories recursively +The command uses a simplified, deterministic approach: -Excluded directories: +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 -- `node_modules`, `.git`, `dist`, `build`, `out`, `coverage` -- `.next`, `.nuxt`, `.output` -- Hidden directories (starting with `.`) +**Important**: Only directories containing a `{name}.webapplication-meta.xml` file are recognized as valid webapps. ### Interactive Selection @@ -393,20 +395,101 @@ Automatically detects Salesforce Code Builder environment and binds to `0.0.0.0` --- +## The `--url` Flag + +The `--url` flag provides control over which dev server URL the proxy uses. It has smart behavior depending on whether the URL is already available. + +### Behavior + +| Scenario | What Happens | +| ------------------------ | ----------------------------------------------------------------- | +| `--url` is reachable | **Proxy-only mode**: Skips starting dev server, only starts proxy | +| `--url` is NOT reachable | Starts dev server, warns if actual URL differs from `--url` | +| No `--url` provided | Starts dev server automatically, detects URL | + +### Use Case 1: Connect to Existing Dev Server (Proxy-Only Mode) + +If you prefer to manage your dev server separately: + +```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! + → Proxy: http://localhost:4545 + → Dev server: http://localhost:5173 +``` + +### Use Case 2: URL Mismatch Warning + +If you specify a `--url` that doesn't match where the dev server actually starts: + +```bash +# No dev server running, specify wrong port +sf webapp dev --url http://localhost:9999 --target-org myOrg +``` + +**Output:** + +``` +Warning: ⚠️ The --url flag (http://localhost:9999) does not match the actual dev server URL (http://localhost:5173/). +The proxy will use the actual dev server URL. +``` + +The command continues working with the actual dev server URL. + +### Important Notes + +- The `--url` flag checks **only** the URL you specify, not other ports +- If you have a dev server on port 5173 but specify `--url http://localhost:9999`: + - Command checks 9999 → not available + - Starts a NEW dev server → may get port 5174 (if 5173 is taken) + - Warns about mismatch (9999 ≠ 5174) +- To use an existing dev server, specify its **exact** URL with `--url` + +--- + ## Troubleshooting -### "No webapplications folder found" +### "No webapp found" or "No valid webapps" -Create a `webapplications/` folder with at least one webapp subfolder: +Ensure your webapp has the required `.webapplication-meta.xml` file: ``` -my-project/ -└── webapplications/ - └── my-app/ - └── package.json +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! ``` -Note: `webapplication.json` is optional! +**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" @@ -586,7 +669,7 @@ plugin-webapp/ | Component | Purpose | | ---------------------- | ------------------------------------------------ | | `dev.ts` | Command orchestration and lifecycle | -| `webappDiscovery.ts` | Recursive webapplication.json discovery | +| `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 | diff --git a/messages/webapp.dev.md b/messages/webapp.dev.md index b84fb8b..6a413b1 100644 --- a/messages/webapp.dev.md +++ b/messages/webapp.dev.md @@ -85,12 +85,12 @@ Proxy URL: %s (open this in your browser) # info.ready-for-development ✅ Ready for development! -→ Proxy: %s (open this in your browser) -→ Dev server: %s + → Proxy: %s (open this in your browser) + → Dev server: %s # info.press-ctrl-c -Press Ctrl+C to stop the server +Press Ctrl+C to stop # info.dev-server-healthy @@ -145,40 +145,35 @@ Auto-selected webapp "%s" (running from inside its folder) ✅ Using webapp: %s (%s) +# info.starting-webapp + +✅ Starting %s + # prompt.select-webapp Select the webapp to run: # warning.no-manifest -No webapplication.json found for webapp "%s" -Location: %s +No webapplication.json found - using defaults (command: %s) -Using defaults: -→ Name: "%s" (derived from folder) -→ Command: "%s" -→ Manifest watching: disabled - -💡 To customize, create a webapplication.json file in your webapp directory. +Tip: See "sf webapp dev --help" for configuration options. # warning.empty-manifest -webapplication.json found for webapp "%s" but has no dev configuration -Location: %s - -Using defaults: -→ Name: "%s" (derived from folder) -→ Command: "%s" -→ Manifest watching: enabled +No dev configuration in webapplication.json - using defaults (command: %s) -💡 To customize, add dev configuration to your webapplication.json file. -Example: -{ -"dev": { -"command": "npm run dev" -} -} +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. diff --git a/src/commands/webapp/dev.ts b/src/commands/webapp/dev.ts index 7514066..6428f77 100644 --- a/src/commands/webapp/dev.ts +++ b/src/commands/webapp/dev.ts @@ -102,6 +102,22 @@ export default class WebappDev extends SfCommand { }); } + /** + * Check if a URL is reachable (returns true/false) + * Used to check if --url is already available before starting dev server + */ + private static async isUrlReachable(url: string): Promise { + try { + const response = await fetch(url, { + method: 'HEAD', + signal: AbortSignal.timeout(3000), // 3 second timeout + }); + return response.ok; + } catch { + return false; + } + } + // eslint-disable-next-line complexity public async run(): Promise { const { flags } = await this.parse(WebappDev); @@ -157,18 +173,12 @@ export default class WebappDev extends SfCommand { const hasDevConfig = manifest?.dev?.url != null || manifest?.dev?.command != null; if (!hasDevConfig) { // Manifest exists but has no dev configuration - show empty manifest warning - this.warn( - messages.getMessage('warning.empty-manifest', [ - selectedWebapp.name, - selectedWebapp.relativePath, - selectedWebapp.name, - DEFAULT_DEV_COMMAND, - ]) - ); + this.warn(messages.getMessage('warning.empty-manifest', [DEFAULT_DEV_COMMAND])); } - // Use selectedWebapp.name (already calculated with folder name fallback during discovery) - this.log(messages.getMessage('info.using-webapp', [selectedWebapp.name, selectedWebapp.relativePath])); + // Show starting message + this.log(''); + this.log(messages.getMessage('info.starting-webapp', [selectedWebapp.name])); this.logger.debug(`Manifest loaded: ${selectedWebapp.name}`); // Setup manifest change handler @@ -204,93 +214,125 @@ export default class WebappDev extends SfCommand { }); } else { // No manifest - show warning and use defaults - this.warn( - messages.getMessage('warning.no-manifest', [ - selectedWebapp.name, - selectedWebapp.relativePath, - selectedWebapp.name, - DEFAULT_DEV_COMMAND, - ]) - ); - this.log(messages.getMessage('info.using-webapp', [selectedWebapp.name, selectedWebapp.relativePath])); + this.warn(messages.getMessage('warning.no-manifest', [DEFAULT_DEV_COMMAND])); + this.log(''); + this.log(messages.getMessage('info.starting-webapp', [selectedWebapp.name])); } // Step 3: Determine dev server URL + // Track whether we should skip starting dev server (when --url is already reachable) + let skipDevServer = false; + let explicitUrlProvided = false; - // Priority: --url flag > manifest dev.url > manifest dev.command > default command (for no-manifest) + // Handle --url flag: check if URL is already reachable before starting dev server if (flags.url) { - devServerUrl = flags.url; - this.logger.debug(`Using explicit dev server URL: ${devServerUrl}`); - } else if (manifest?.dev?.url) { - devServerUrl = manifest.dev.url; - this.logger.debug(`Using dev server URL from manifest: ${devServerUrl}`); - } else { - // Determine command: from manifest or default - const devCommand = manifest?.dev?.command ?? DEFAULT_DEV_COMMAND; - - if (!selectedWebapp.hasManifest) { - this.logger.debug(messages.getMessage('info.using-defaults', [devCommand])); + explicitUrlProvided = true; + this.logger.debug(`Checking if explicit URL is reachable: ${flags.url}`); + + const isReachable = await WebappDev.isUrlReachable(flags.url); + + if (isReachable) { + // URL is already available - skip starting dev server, only start proxy + devServerUrl = flags.url; + skipDevServer = true; + this.log(messages.getMessage('info.url-already-available', [flags.url])); + this.logger.debug(`URL ${flags.url} is reachable, skipping dev server startup`); + } else { + // URL not reachable - will start dev server and check for mismatch later + this.logger.debug(`URL ${flags.url} is not reachable, will start dev server`); } + } - // Start dev server from the webapp directory - this.logger.debug(`Starting dev server with command: ${devCommand}`); - this.devServerManager = new DevServerManager({ - command: devCommand, - cwd: webappDir, - }); + // If we're not skipping dev server, determine how to start it + if (!skipDevServer) { + if (manifest?.dev?.url && !explicitUrlProvided) { + // Use manifest dev.url + devServerUrl = manifest.dev.url; + this.logger.debug(`Using dev server URL from manifest: ${devServerUrl}`); + } else { + // Start dev server with command + const devCommand = manifest?.dev?.command ?? DEFAULT_DEV_COMMAND; + + if (!selectedWebapp.hasManifest) { + this.logger.debug(messages.getMessage('info.using-defaults', [devCommand])); + } - // Setup dev server event handlers - this.devServerManager.on('ready', (url: string) => { - this.logger?.debug(`Dev server ready at: ${url}`); - // Clear any dev server error when server starts successfully - this.proxyServer?.clearActiveDevServerError(); - }); + // Start dev server from the webapp directory + this.logger.debug(`Starting dev server with command: ${devCommand}`); + this.devServerManager = new DevServerManager({ + command: devCommand, + cwd: webappDir, + }); - this.devServerManager.on('error', (error: SfError | DevServerError) => { - // Set error for proxy to display in browser (if proxy is running) - // Don't log here - the error will be thrown and displayed by the main catch block - if ('stderrLines' in error && Array.isArray(error.stderrLines) && 'title' in error && 'type' in error) { - this.proxyServer?.setActiveDevServerError(error); - } - this.logger?.debug(`Dev server error: ${error.message}`); - }); + // Setup dev server event handlers + this.devServerManager.on('ready', (url: string) => { + this.logger?.debug(`Dev server ready at: ${url}`); + // Clear any dev server error when server starts successfully + this.proxyServer?.clearActiveDevServerError(); + }); - this.devServerManager.on('exit', () => { - this.logger?.debug('Dev server stopped'); - }); + this.devServerManager.on('error', (error: SfError | DevServerError) => { + // Set error for proxy to display in browser (if proxy is running) + // Don't log here - the error will be thrown and displayed by the main catch block + if ('stderrLines' in error && Array.isArray(error.stderrLines) && 'title' in error && 'type' in error) { + this.proxyServer?.setActiveDevServerError(error); + } + this.logger?.debug(`Dev server error: ${error.message}`); + }); - this.devServerManager.start(); - - // Wait for dev server to be ready - devServerUrl = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject( - new SfError('Dev server did not start within 30 seconds.', 'DevServerTimeoutError', [ - 'The dev server may be taking longer than expected to start', - 'Check if the dev server command is correct in webapplication.json', - 'Try running the dev server command manually to see if it starts', - ]) - ); - }, 30_000); - - this.devServerManager?.on('ready', (url: string) => { - clearTimeout(timeout); - resolve(url); + this.devServerManager.on('exit', () => { + this.logger?.debug('Dev server stopped'); }); - this.devServerManager?.on('error', (error: SfError) => { - clearTimeout(timeout); - reject(error); + this.devServerManager.start(); + + // Wait for dev server to be ready + const actualDevServerUrl = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject( + new SfError('Dev server did not start within 30 seconds.', 'DevServerTimeoutError', [ + 'The dev server may be taking longer than expected to start', + 'Check if the dev server command is correct in webapplication.json', + 'Try running the dev server command manually to see if it starts', + ]) + ); + }, 30_000); + + this.devServerManager?.on('ready', (url: string) => { + clearTimeout(timeout); + resolve(url); + }); + + this.devServerManager?.on('error', (error: SfError) => { + clearTimeout(timeout); + reject(error); + }); }); - }); + + // Check for URL mismatch if --url was provided + if (explicitUrlProvided && flags.url && flags.url !== actualDevServerUrl) { + this.warn(messages.getMessage('warning.url-mismatch', [flags.url, actualDevServerUrl])); + } + + // Use the actual dev server URL + devServerUrl = actualDevServerUrl; + } } - // Step 3: Get org info for authentication + // Step 4: Get org info for authentication const orgConnection = flags['target-org'].getConnection(undefined); orgUsername = flags['target-org'].getUsername() ?? orgConnection.getUsername() ?? 'unknown'; this.logger.debug(`Using authentication for org: ${orgUsername}`); - // Step 4: Start proxy server + // 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.', + 'DevServerUrlError' + ); + } + + // Step 5: Start proxy server this.logger.debug(`Starting proxy server on port ${flags.port}...`); const salesforceInstanceUrl = orgConnection.instanceUrl; this.proxyServer = new ProxyServer({ @@ -315,12 +357,12 @@ export default class WebappDev extends SfCommand { this.log(messages.getMessage('info.start-dev-server-hint')); }); - // Step 5: Check if dev server is reachable (non-blocking warning) + // Step 6: Check if dev server is reachable (non-blocking warning) if (devServerUrl) { await this.checkDevServerHealth(devServerUrl); } - // Step 6: Open browser if requested + // Step 7: Open browser if requested if (flags.open) { this.logger.debug('Opening browser...'); await WebappDev.openBrowser(proxyUrl); diff --git a/src/config/webappDiscovery.ts b/src/config/webappDiscovery.ts index 3c756f0..e0b8bd4 100644 --- a/src/config/webappDiscovery.ts +++ b/src/config/webappDiscovery.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { readdir, readFile } from 'node:fs/promises'; +import { access, readdir, readFile } from 'node:fs/promises'; import { basename, dirname, join, relative } from 'node:path'; -import { SfError } from '@salesforce/core'; +import { SfError, SfProject } from '@salesforce/core'; import type { WebAppManifest } from './manifest.js'; /** @@ -25,9 +25,14 @@ import type { WebAppManifest } from './manifest.js'; export const DEFAULT_DEV_COMMAND = 'npm run dev'; /** - * Pattern to match the webapplications folder (case-insensitive) + * Standard SFDX path to webapplications folder relative to project root */ -const WEBAPPLICATIONS_FOLDER_PATTERN = /^webapplications$/i; +const SFDX_WEBAPPLICATIONS_PATH = 'force-app/main/default/webapplications'; + +/** + * Pattern to match webapplication metadata XML files + */ +const WEBAPP_META_XML_PATTERN = /^(.+)\.webapplication-meta\.xml$/; /** * Discovered webapp with its directory path and optional manifest @@ -45,43 +50,59 @@ export type DiscoveredWebapp = { 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) */ + hasMetaXml: boolean; }; /** - * Directories to exclude when searching for webapplications folder - */ -const EXCLUDED_DIRECTORIES = new Set([ - 'node_modules', - '.git', - 'dist', - 'build', - 'out', - 'coverage', - '.next', - '.nuxt', - '.output', - '__pycache__', - '.venv', - 'venv', -]); - -/** - * Maximum depth to search for webapplications folder + * Directories to exclude when processing webapplications folder. + * Note: Directories starting with '.' are excluded separately in shouldExcludeDirectory() */ -const MAX_SEARCH_DEPTH = 10; +const EXCLUDED_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'out', 'coverage', '__pycache__', 'venv']); /** - * Check if a directory should be excluded from search + * Check if a directory should be excluded from processing */ function shouldExcludeDirectory(dirName: string): boolean { return EXCLUDED_DIRECTORIES.has(dirName) || dirName.startsWith('.'); } /** - * Check if a folder name matches "webapplications" (case-insensitive) + * Check if a folder name is the standard webapplications folder */ function isWebapplicationsFolder(folderName: string): boolean { - return WEBAPPLICATIONS_FOLDER_PATTERN.test(folderName); + return folderName === basename(SFDX_WEBAPPLICATIONS_PATH); +} + +/** + * Check if a directory contains a {name}.webapplication-meta.xml file + * Returns the webapp name extracted from the filename, or null if not found + */ +async function findWebappMetaXml(dirPath: string): Promise { + try { + const entries = await readdir(dirPath); + for (const entry of entries) { + const match = WEBAPP_META_XML_PATTERN.exec(entry); + if (match) { + return match[1]; // Return the webapp name from the filename + } + } + return null; + } catch { + return null; + } +} + +/** + * Check if a path exists + */ +async function pathExists(path: string): Promise { + try { + await access(path); + return true; + } catch { + return false; + } } /** @@ -105,37 +126,32 @@ async function tryParseWebappManifest(filePath: string): Promise metaXmlName > folderName * - * @param dir - Directory to search in - * @param depth - Current search depth - * @returns Path to webapplications folder or null if not found + * @param folderName - The folder name (fallback) + * @param metaXmlName - Name extracted from .webapplication-meta.xml (or null) + * @param manifest - Parsed manifest (or null) + * @returns The resolved webapp name */ -async function findWebapplicationsFolderRecursive(dir: string, depth: number = 0): Promise { - if (depth > MAX_SEARCH_DEPTH) { - return null; +function resolveWebappName(folderName: string, metaXmlName: string | null, manifest: WebAppManifest | null): string { + if (manifest?.name && typeof manifest.name === 'string' && manifest.name.trim()) { + return manifest.name; } + return metaXmlName ?? folderName; +} +/** + * Try to resolve SFDX project root from a given directory. + * Uses SfProject from @salesforce/core which walks up directories to find sfdx-project.json. + * + * @param cwd - Directory to start from + * @returns Project root path or null if not in an SFDX project + */ +async function tryResolveSfdxProjectRoot(cwd: string): Promise { try { - const entries = await readdir(dir, { withFileTypes: true }); - - // Check if any directory at this level is "webapplications" (case-insensitive) - const webappsFolder = entries.find((e) => e.isDirectory() && isWebapplicationsFolder(e.name)); - if (webappsFolder) { - return join(dir, webappsFolder.name); - } - - // Recursively search subdirectories in parallel - const subdirectories = entries.filter((e) => e.isDirectory() && !shouldExcludeDirectory(e.name)); - - const results = await Promise.all( - subdirectories.map((subdir) => findWebapplicationsFolderRecursive(join(dir, subdir.name), depth + 1)) - ); - - // Return the first non-null result - return results.find((result) => result !== null) ?? null; + return await SfProject.resolveProjectPath(cwd); } catch { - // Permission denied or other read error - skip this directory + // Not in an SFDX project return null; } } @@ -145,21 +161,17 @@ async function findWebapplicationsFolderRecursive(dir: string, depth: number = 0 * * This handles cases where the user runs the command from inside a webapp folder: * - * Example 1: Running from /project/webapplications/my-app/src/ + * 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" } + * Returns: { webappsFolder: "/project/.../webapplications", currentWebappName: "my-app" } * - * Example 2: Running from /project/webapplications/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" } + * Returns: { webappsFolder: "/project/.../webapplications", currentWebappName: "my-app" } * - * Example 3: Running from /project/webapplications/ + * Example 3: Running from /project/force-app/main/default/webapplications/ * Current dir is webapplications (found!) - * Returns: { webappsFolder: "/project/webapplications", currentWebappName: null } - * - * Example 4: Running from /project/src/ - * Traverses: src -> project -> / (not found) - * Returns: null (will fall back to downward search) + * Returns: { webappsFolder: "/project/.../webapplications", currentWebappName: null } * * @param dir - Directory to start from * @returns Object with webapplications folder path and current webapp name, or null if not found @@ -211,13 +223,13 @@ function findWebapplicationsFolderUpward( } /** - * Discover all webapps inside the webapplications folder - * Each subdirectory is treated as a webapp. If a webapplication.json exists, use it. - * Otherwise, use the folder name as the webapp name with defaults. + * 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. * * @param webappsFolderPath - Absolute path to the webapplications folder * @param cwd - Original working directory for relative path calculation - * @returns Array of discovered webapps + * @returns Array of discovered webapps (only those with .webapplication-meta.xml) */ async function discoverWebappsInFolder(webappsFolderPath: string, cwd: string): Promise { try { @@ -227,41 +239,37 @@ async function discoverWebappsInFolder(webappsFolderPath: string, cwd: string): const webappDirs = entries.filter((e) => e.isDirectory() && !shouldExcludeDirectory(e.name)); // Process each webapp directory in parallel - const webappPromises = webappDirs.map(async (entry): Promise => { + const webappPromises = webappDirs.map(async (entry): Promise => { const webappPath = join(webappsFolderPath, entry.name); + + // Check for .webapplication-meta.xml file - this identifies valid webapps + const metaXmlName = await findWebappMetaXml(webappPath); + + // Only include directories that have a .webapplication-meta.xml file + if (!metaXmlName) { + return null; + } + const manifestFilePath = join(webappPath, 'webapplication.json'); - // Try to load manifest + // Try to load manifest for dev configuration const manifest = await tryParseWebappManifest(manifestFilePath); - if (manifest) { - // Webapp has manifest file - use manifest data with folder name as fallback - // Name: use manifest.name if present, otherwise folder name - const webappName = - manifest.name && typeof manifest.name === 'string' && manifest.name.trim() ? manifest.name : entry.name; - - return { - path: webappPath, - relativePath: relative(cwd, webappPath) || entry.name, - manifest, - name: webappName, - hasManifest: true, - manifestPath: manifestFilePath, - }; - } else { - // No manifest file - use folder name and defaults - return { - path: webappPath, - relativePath: relative(cwd, webappPath) || entry.name, - manifest: null, - name: entry.name, - hasManifest: false, - manifestPath: null, - }; - } + return { + path: webappPath, + relativePath: relative(cwd, webappPath) || entry.name, + manifest, + name: resolveWebappName(entry.name, metaXmlName, manifest), + hasManifest: manifest !== null, + manifestPath: manifest ? manifestFilePath : null, + hasMetaXml: true, + }; }); - return await Promise.all(webappPromises); + const results = await Promise.all(webappPromises); + + // Filter out null results (directories without .webapplication-meta.xml) + return results.filter((webapp): webapp is DiscoveredWebapp => webapp !== null); } catch { // Permission denied or other read error return []; @@ -269,41 +277,91 @@ async function discoverWebappsInFolder(webappsFolderPath: string, cwd: string): } /** - * Result of finding all webapps, includes hint for auto-selection + * Result of finding all webapps, includes context about current location */ type FindAllWebappsResult = { /** All discovered webapps */ webapps: DiscoveredWebapp[]; - /** Name of webapp user is currently inside (for auto-selection), null if not inside any */ + /** 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) */ + /** Whether the webapplications folder was found (even if empty or no valid webapps) */ webappsFolderFound: boolean; + /** Whether we're in an SFDX project context */ + inSfdxProject: boolean; }; /** - * Find all webapps in the webapplications folder. - * Also returns a hint if the user is currently inside a specific webapp folder. + * Find all webapps using simplified discovery algorithm. + * + * Discovery strategy (in order): + * 1. Check if inside a webapplications/ directory (upward search) + * 2. Check for SFDX project and use fixed path: force-app/main/default/webapplications + * 3. If neither, check if current directory is a webapp (has .webapplication-meta.xml) * * @param cwd - Directory to search from (defaults to process.cwd()) - * @returns Object with discovered webapps and currentWebappName hint for auto-selection + * @returns Object with discovered webapps and context information */ async function findAllWebapps(cwd: string = process.cwd()): Promise { - // Step 1: Check upward first - this gives us currentWebappName if inside a webapp - const upwardResult = findWebapplicationsFolderUpward(cwd); - let webappsFolder: string | null = null; let currentWebappName: 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); if (upwardResult) { webappsFolder = upwardResult.webappsFolder; currentWebappName = upwardResult.currentWebappName; } else { - // Step 2: Search downward if not found upward - webappsFolder = await findWebapplicationsFolderRecursive(cwd); + // Step 2: Check for SFDX project and use fixed path + const projectRoot = await tryResolveSfdxProjectRoot(cwd); + + if (projectRoot) { + inSfdxProject = true; + const sfdxWebappsPath = join(projectRoot, SFDX_WEBAPPLICATIONS_PATH); + + if (await pathExists(sfdxWebappsPath)) { + webappsFolder = sfdxWebappsPath; + } + } } + // 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) { - return { webapps: [], currentWebappName: null, webappsFolderFound: false }; + const metaXmlName = await findWebappMetaXml(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, manifest); + + const standaloneWebapp: DiscoveredWebapp = { + path: cwd, + relativePath: '.', + manifest, + name: webappName, + hasManifest: manifest !== null, + manifestPath: manifest ? manifestFilePath : null, + hasMetaXml: true, + }; + + return { + webapps: [standaloneWebapp], + currentWebappName: webappName, + webappsFolderFound: false, + inSfdxProject: false, + }; + } + + // No webapp found anywhere + return { + webapps: [], + currentWebappName: null, + webappsFolderFound: false, + inSfdxProject, + }; } // Discover all webapps in the folder @@ -314,14 +372,15 @@ async function findAllWebapps(cwd: string = process.cwd()): Promise a.name.localeCompare(b.name)), currentWebappName, webappsFolderFound: true, + inSfdxProject, }; } /** * Result of webapp discovery */ -type DiscoverWebappResult = { - /** The selected/discovered webapp (null if user needs to select) */ +export type DiscoverWebappResult = { + /** The selected/discovered webapp (null if user needs to select via prompt) */ webapp: DiscoveredWebapp | null; /** All discovered webapps */ allWebapps: DiscoveredWebapp[]; @@ -332,60 +391,94 @@ type DiscoverWebappResult = { /** * Get a single webapp, handling the various discovery scenarios. * - * Selection priority: - * 1. If --name flag provided, find that specific webapp - * 2. If user is inside a webapp folder, auto-select that webapp - * 3. If only one webapp exists, auto-select it - * 4. If multiple webapps, return null (user must select) + * Discovery use cases: + * 1. SFDX Project Root: Check fixed path force-app/main/default/webapplications + * - Webapps identified by {name}.webapplication-meta.xml + * - Always prompt for selection (even if only 1 webapp) + * + * 2. Inside webapplications/ directory: + * - Auto-select current webapp + * - Error if --name conflicts with current directory + * + * 3. Outside SFDX project with .webapplication-meta.xml in current dir: + * - Use current directory as standalone webapp * - * @param name - Optional webapp name to search for + * @param name - Optional webapp 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 or named webapp not found + * @throws SfError if no webapps found, named webapp not found, or --name conflicts with current dir */ export async function discoverWebapp( name: string | undefined, cwd: string = process.cwd() ): Promise { - const { webapps: allWebapps, currentWebappName, webappsFolderFound } = await findAllWebapps(cwd); + const { webapps: allWebapps, currentWebappName, webappsFolderFound, inSfdxProject } = await findAllWebapps(cwd); // No webapps found if (allWebapps.length === 0) { if (webappsFolderFound) { - // Folder exists but is empty + // Folder exists but no valid webapps (no .webapplication-meta.xml files) throw new SfError( - 'Found "webapplications" folder but no webapps inside it.\n' + - 'Create webapp subdirectories inside the "webapplications" folder to get started.\n\n' + + 'Found "webapplications" folder but no valid webapps inside it.\n' + + 'Each webapp must have a {name}.webapplication-meta.xml file.\n\n' + 'Expected structure:\n' + ' webapplications/\n' + - ' ├── my-app-1/\n' + - ' │ └── webapplication.json (optional)\n' + - ' └── my-app-2/', + ' └── my-app/\n' + + ' ├── my-app.webapplication-meta.xml (required)\n' + + ' └── webapplication.json (optional, for dev config)', + 'WebappNotFoundError' + ); + } else if (inSfdxProject) { + // In SFDX project but webapplications folder doesn't exist + throw new SfError( + 'No webapplications folder found in the SFDX project.\n' + + `Expected location: ${SFDX_WEBAPPLICATIONS_PATH}\n\n` + + 'Create the folder structure:\n' + + ' force-app/main/default/webapplications/\n' + + ' └── my-app/\n' + + ' ├── my-app.webapplication-meta.xml (required)\n' + + ' └── webapplication.json (optional, for dev config)', 'WebappNotFoundError' ); } else { - // Folder doesn't exist + // Not in SFDX project and no webapp found throw new SfError( - 'No webapplications folder found in the current directory or subdirectories.\n' + - 'Create a "webapplications" folder with webapp subdirectories to get started.\n\n' + - 'Expected structure:\n' + - ' webapplications/\n' + - ' ├── my-app-1/\n' + - ' │ └── webapplication.json (optional)\n' + - ' └── my-app-2/', + 'No webapp found.\n\n' + + 'To use this command, either:\n' + + '1. Run from an SFDX project with webapps in force-app/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' ); } } - // Priority 1: If name is provided via --name flag, find that specific webapp + // 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 (currentWebapp && currentWebapp.name !== name && basename(currentWebapp.path) !== name) { + throw new SfError( + `You are inside the "${currentWebappName}" webapp 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` + + ' - Run the command from the project root to use --name', + 'WebappNameConflictError' + ); + } + } + + // Priority 1: If --name flag provided, find that specific webapp if (name) { - const webapp = allWebapps.find((w) => w.name === name); + const webapp = allWebapps.find((w) => w.name === name || basename(w.path) === name); if (!webapp) { const WARNING = '\u26A0\uFE0F'; // ⚠️ const availableNames = allWebapps - .map((w) => ` - ${w.name} - (Path:${w.relativePath})${w.hasManifest ? '' : ` - ${WARNING} No Manifest`}`) + .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}`, @@ -396,7 +489,6 @@ export async function discoverWebapp( } // Priority 2: If user is inside a webapp folder, auto-select that webapp - // Match by webapp.name OR by folder name (extracted from path) if (currentWebappName) { const webapp = allWebapps.find((w) => w.name === currentWebappName || basename(w.path) === currentWebappName); if (webapp) { @@ -404,11 +496,7 @@ export async function discoverWebapp( } } - // Priority 3: If only one webapp exists, auto-select it - if (allWebapps.length === 1) { - return { webapp: allWebapps[0], allWebapps, autoSelected: false }; - } - - // Multiple webapps found - return null to indicate selection is needed + // 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 }; } diff --git a/src/error/DevServerErrorParser.ts b/src/error/DevServerErrorParser.ts index 4d1009b..f2d380f 100644 --- a/src/error/DevServerErrorParser.ts +++ b/src/error/DevServerErrorParser.ts @@ -189,8 +189,9 @@ export class DevServerErrorParser { } } - // Fallback (should never reach here due to catch-all pattern) - return this.createGenericError(stderr, exitCode, signal); + // This point is unreachable because the last pattern (.*) matches everything + // TypeScript requires a return statement, so we throw an error for safety + throw new Error('Unreachable: ERROR_PATTERNS catch-all should always match'); } /** @@ -216,29 +217,4 @@ export class DevServerErrorParser { // Return last N lines (most recent errors) return lines.slice(-maxLines); } - - /** - * Create a generic error when no specific pattern matches - * - * @param stderr - Full stderr output - * @param exitCode - Process exit code - * @param signal - Process exit signal - * @returns Generic DevServerError - */ - private static createGenericError(stderr: string, exitCode?: number | null, signal?: string | null): DevServerError { - return { - type: 'unknown', - title: 'Dev Server Failed to Start', - message: 'The dev server encountered an error. Check the error output below for details.', - stderrLines: this.extractRelevantLines(stderr, 15), - suggestions: [ - 'Review the error output above', - 'Try running your dev command manually to debug', - 'Verify your project setup is correct', - ], - fullStderr: stderr, - exitCode, - signal, - }; - } } diff --git a/src/server/DevServerManager.ts b/src/server/DevServerManager.ts index bbbe344..b302274 100644 --- a/src/server/DevServerManager.ts +++ b/src/server/DevServerManager.ts @@ -346,9 +346,11 @@ export class DevServerManager extends EventEmitter { // Emit output event for consumers this.emit(stream, output); + // Split lines once and reuse for all operations + const lines = output.split('\n').filter((line) => line.trim()); + // Capture stderr lines for error parsing if (stream === 'stderr') { - const lines = output.split('\n').filter((line) => line.trim()); this.stderrBuffer.push(...lines); // Keep only the last N lines to prevent memory issues @@ -358,7 +360,6 @@ export class DevServerManager extends EventEmitter { } // Log dev server output (only visible when SF_LOG_LEVEL=debug) - const lines = output.split('\n').filter((line) => line.trim()); for (const line of lines) { this.logger.debug(`[Dev Server ${stream}] ${line}`); } diff --git a/test/config/webappDiscovery.test.ts b/test/config/webappDiscovery.test.ts index 5b9aac0..a8ea7fe 100644 --- a/test/config/webappDiscovery.test.ts +++ b/test/config/webappDiscovery.test.ts @@ -17,27 +17,69 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { expect } from 'chai'; -import { SfError } from '@salesforce/core'; -import { TestContext } from '@salesforce/core/testSetup'; +import { SfError, SfProject } from '@salesforce/core'; import { DEFAULT_DEV_COMMAND, discoverWebapp } from '../../src/config/webappDiscovery.js'; describe('webappDiscovery', () => { - const $$ = new TestContext(); const testDir = join(process.cwd(), '.test-webapp-discovery'); + // Standard SFDX webapplications path + const sfdxWebappsPath = join(testDir, 'force-app', 'main', 'default', 'webapplications'); + + // Store original resolveProjectPath + let originalResolveProjectPath: typeof SfProject.resolveProjectPath; + + /** + * Helper to create a valid webapp directory with required .webapplication-meta.xml + */ + function createWebapp(webappsPath: string, name: string, manifest?: object): string { + const appPath = join(webappsPath, name); + mkdirSync(appPath, { recursive: true }); + // Create required .webapplication-meta.xml file + writeFileSync(join(appPath, `${name}.webapplication-meta.xml`), ''); + if (manifest) { + writeFileSync(join(appPath, 'webapplication.json'), JSON.stringify(manifest)); + } + return appPath; + } + + /** + * Helper to setup SFDX project structure and mock SfProject.resolveProjectPath + */ + function setupSfdxProject(): void { + // Create SFDX project structure + mkdirSync(sfdxWebappsPath, { recursive: true }); + writeFileSync(join(testDir, 'sfdx-project.json'), '{}'); + // Mock SfProject.resolveProjectPath to return testDir + SfProject.resolveProjectPath = async () => testDir; + } + + /** + * Helper to mock SfProject.resolveProjectPath to throw (not in SFDX project) + */ + function mockNotInSfdxProject(): void { + SfProject.resolveProjectPath = async () => { + throw new Error('Not in SFDX project'); + }; + } + beforeEach(() => { + // Store original - eslint-disable needed because we're intentionally storing the method for mocking + // eslint-disable-next-line @typescript-eslint/unbound-method + originalResolveProjectPath = SfProject.resolveProjectPath; // Create test directory mkdirSync(testDir, { recursive: true }); }); afterEach(() => { + // Restore original + SfProject.resolveProjectPath = originalResolveProjectPath; // Clean up test directory try { rmSync(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } - $$.restore(); }); describe('DEFAULT_DEV_COMMAND', () => { @@ -47,20 +89,38 @@ describe('webappDiscovery', () => { }); describe('discoverWebapp', () => { - it('should throw error if no webapplications folder found', async () => { + it('should throw error if no webapp found (not in SFDX project)', async () => { + mockNotInSfdxProject(); + try { await discoverWebapp(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'); + expect((error as SfError).message).to.include('No webapp found'); } }); - it('should throw error if webapplications folder exists but is empty', async () => { - const webappsPath = join(testDir, 'webapplications'); - mkdirSync(webappsPath, { recursive: true }); + it('should throw error if SFDX project has no webapplications folder', async () => { + // Create SFDX project but NOT the webapplications folder + writeFileSync(join(testDir, 'sfdx-project.json'), '{}'); + SfProject.resolveProjectPath = async () => testDir; + + try { + await discoverWebapp(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'); + } + }); + + it('should throw error if webapplications folder exists but has no valid webapps', async () => { + setupSfdxProject(); + // Create directory without .webapplication-meta.xml + mkdirSync(join(sfdxWebappsPath, 'invalid-app'), { recursive: true }); try { await discoverWebapp(undefined, testDir); @@ -68,15 +128,14 @@ describe('webappDiscovery', () => { } catch (error) { expect(error).to.be.instanceOf(SfError); expect((error as SfError).name).to.equal('WebappNotFoundError'); - expect((error as SfError).message).to.include('Found "webapplications" folder but no webapps inside it'); - expect((error as SfError).message).to.not.include('No webapplications folder found'); + expect((error as SfError).message).to.include('no valid webapps'); } }); it('should find webapp by name when provided', async () => { - const webappsPath = join(testDir, 'webapplications'); - mkdirSync(join(webappsPath, 'app-a'), { recursive: true }); - mkdirSync(join(webappsPath, 'app-b'), { recursive: true }); + setupSfdxProject(); + createWebapp(sfdxWebappsPath, 'app-a'); + createWebapp(sfdxWebappsPath, 'app-b'); const result = await discoverWebapp('app-b', testDir); @@ -86,8 +145,8 @@ describe('webappDiscovery', () => { }); it('should throw error if named webapp not found', async () => { - const webappsPath = join(testDir, 'webapplications'); - mkdirSync(join(webappsPath, 'my-app'), { recursive: true }); + setupSfdxProject(); + createWebapp(sfdxWebappsPath, 'my-app'); try { await discoverWebapp('non-existent', testDir); @@ -102,9 +161,9 @@ describe('webappDiscovery', () => { it('should auto-select webapp when inside its folder', async () => { const webappsPath = join(testDir, 'webapplications'); - const myAppPath = join(webappsPath, 'my-app'); - mkdirSync(myAppPath, { recursive: true }); - mkdirSync(join(webappsPath, 'other-app'), { recursive: true }); + mkdirSync(webappsPath, { recursive: true }); + const myAppPath = createWebapp(webappsPath, 'my-app'); + createWebapp(webappsPath, 'other-app'); const result = await discoverWebapp(undefined, myAppPath); @@ -114,10 +173,11 @@ describe('webappDiscovery', () => { it('should auto-select webapp when inside subfolder', async () => { const webappsPath = join(testDir, 'webapplications'); - const myAppPath = join(webappsPath, 'my-app'); + mkdirSync(webappsPath, { recursive: true }); + const myAppPath = createWebapp(webappsPath, 'my-app'); const srcPath = join(myAppPath, 'src'); mkdirSync(srcPath, { recursive: true }); - mkdirSync(join(webappsPath, 'other-app'), { recursive: true }); + createWebapp(webappsPath, 'other-app'); const result = await discoverWebapp(undefined, srcPath); @@ -125,13 +185,11 @@ describe('webappDiscovery', () => { expect(result.autoSelected).to.be.true; }); - it('should auto-select by folder name when manifest name differs', async () => { + it('should use manifest name when available', async () => { const webappsPath = join(testDir, 'webapplications'); - const myAppPath = join(webappsPath, 'folder-name'); - mkdirSync(myAppPath, { recursive: true }); - mkdirSync(join(webappsPath, 'other-app'), { recursive: true }); - - writeFileSync(join(myAppPath, 'webapplication.json'), JSON.stringify({ name: 'ManifestName' })); + mkdirSync(webappsPath, { recursive: true }); + const myAppPath = createWebapp(webappsPath, 'folder-name', { name: 'ManifestName' }); + createWebapp(webappsPath, 'other-app'); const result = await discoverWebapp(undefined, myAppPath); @@ -139,20 +197,22 @@ describe('webappDiscovery', () => { expect(result.autoSelected).to.be.true; }); - it('should auto-select single webapp', async () => { - const webappsPath = join(testDir, 'webapplications'); - mkdirSync(join(webappsPath, 'only-app'), { recursive: true }); + it('should return null webapp for single webapp at project root (always prompt)', async () => { + setupSfdxProject(); + createWebapp(sfdxWebappsPath, 'only-app'); const result = await discoverWebapp(undefined, testDir); - expect(result.webapp?.name).to.equal('only-app'); + // Now returns null to prompt even for single webapp (reviewer feedback) + expect(result.webapp).to.be.null; expect(result.autoSelected).to.be.false; + expect(result.allWebapps).to.have.length(1); }); it('should return null webapp for multiple webapps (selection needed)', async () => { - const webappsPath = join(testDir, 'webapplications'); - mkdirSync(join(webappsPath, 'app-a'), { recursive: true }); - mkdirSync(join(webappsPath, 'app-b'), { recursive: true }); + setupSfdxProject(); + createWebapp(sfdxWebappsPath, 'app-a'); + createWebapp(sfdxWebappsPath, 'app-b'); const result = await discoverWebapp(undefined, testDir); @@ -161,16 +221,69 @@ describe('webappDiscovery', () => { expect(result.allWebapps).to.have.length(2); }); - it('should prioritize --name flag over auto-selection', async () => { + it('should throw error when --name conflicts with current webapp directory', async () => { const webappsPath = join(testDir, 'webapplications'); - const currentApp = join(webappsPath, 'current-app'); - mkdirSync(currentApp, { recursive: true }); - mkdirSync(join(webappsPath, 'other-app'), { recursive: true }); + mkdirSync(webappsPath, { recursive: true }); + const currentAppPath = createWebapp(webappsPath, 'current-app'); + createWebapp(webappsPath, 'other-app'); + + try { + // Inside current-app but specifying --name other-app + await discoverWebapp('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).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'); - const result = await discoverWebapp('other-app', currentApp); + // Inside current-app and specifying --name current-app (should work) + const result = await discoverWebapp('current-app', currentAppPath); - expect(result.webapp?.name).to.equal('other-app'); + expect(result.webapp?.name).to.equal('current-app'); expect(result.autoSelected).to.be.false; }); + + it('should recognize webapp by .webapplication-meta.xml file', async () => { + setupSfdxProject(); + + // Create directory with .webapplication-meta.xml + const validAppPath = join(sfdxWebappsPath, 'valid-app'); + mkdirSync(validAppPath, { recursive: true }); + writeFileSync(join(validAppPath, 'valid-app.webapplication-meta.xml'), ''); + + // Create directory without .webapplication-meta.xml (should be ignored) + const invalidAppPath = join(sfdxWebappsPath, 'invalid-app'); + mkdirSync(invalidAppPath, { recursive: true }); + + const result = await discoverWebapp(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; + }); + + it('should use standalone webapp when current dir has .webapplication-meta.xml', async () => { + mockNotInSfdxProject(); + + // Create a standalone webapp directory (not in webapplications folder) + const standaloneDir = join(testDir, 'standalone-app'); + mkdirSync(standaloneDir, { recursive: true }); + writeFileSync(join(standaloneDir, 'standalone-app.webapplication-meta.xml'), ''); + + const result = await discoverWebapp(undefined, standaloneDir); + + expect(result.webapp?.name).to.equal('standalone-app'); + expect(result.allWebapps).to.have.length(1); + }); }); }); From 9b9b08e0ebad6ccf0f7210da6b63bbe699c51430 Mon Sep 17 00:00:00 2001 From: Ankit Singh Date: Wed, 11 Feb 2026 13:37:04 +0530 Subject: [PATCH 02/15] feat: W-21111977 consume error-page template from @salesforce/webapp-experimental/proxy (#22) * feat: consume error-page template from @salesforce/webapp-experimental/proxy @W-21111977@ Update plugin-webapp to import the error page HTML template from the @salesforce/webapp-experimental proxy package instead of bundling it locally. Migration changes: - ErrorPageRenderer now uses getErrorPageTemplate() from @salesforce/webapp-experimental/proxy - Removed local error-page.html template (now lives in webapps package) - Removed scripts/copy-templates.cjs (no longer needed) - Removed postbuild script from package.json - Bumped @salesforce/webapp-experimental dependency to ^1.17.0 Bug fixes included: - DevServerManager: emit DevServerError directly instead of wrapping in SfError, so the proxy can display the "Dev Server Error" page when dev server crashes - ProxyServer: add socket error handler to prevent ECONNRESET from crashing the proxy when dev server dies mid-connection Depends on: salesforce-experience-platform-emu/webapps PR for @W-21111977@ Co-authored-by: Cursor * feat: W-21111977 consume error-page template from @salesforce/webapp-experimental/proxy Update plugin-webapp to import the error page HTML template from the @salesforce/webapp-experimental proxy package (v1.23.0) instead of bundling it locally. Migration changes: - ErrorPageRenderer now uses getErrorPageTemplate() from @salesforce/webapp-experimental/proxy (direct import, no workarounds) - Removed local error-page.html template (now lives in webapps package) - Removed scripts/copy-templates.cjs (no longer needed) - Removed postbuild script from package.json - Bumped @salesforce/webapp-experimental dependency to ^1.23.0 Bug fixes included: - DevServerManager: emit DevServerError directly instead of wrapping in SfError, so the proxy can display the "Dev Server Error" page when dev server crashes - ProxyServer: add socket error handler to prevent ECONNRESET from crashing the proxy when dev server dies mid-connection - ProxyServer: remove label/version from fallback manifest (not in type) Depends on: salesforce-experience-platform-emu/webapps#86 (merged) Co-authored-by: Cursor --------- Co-authored-by: Ankit Singh Co-authored-by: Cursor --- package.json | 3 +- scripts/copy-templates.cjs | 31 -- src/proxy/ProxyServer.ts | 7 +- src/server/DevServerManager.ts | 9 +- src/templates/ErrorPageRenderer.ts | 54 +- src/templates/error-page.html | 749 ---------------------------- test/commands/webapp/dev.test.ts | 11 - test/config/ManifestWatcher.test.ts | 25 +- test/config/types.test.ts | 20 +- test/proxy/ProxyServer.test.ts | 6 - yarn.lock | 23 +- 11 files changed, 40 insertions(+), 898 deletions(-) delete mode 100644 scripts/copy-templates.cjs delete mode 100644 src/templates/error-page.html diff --git a/package.json b/package.json index 303a9d5..4acd8bb 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@salesforce/core": "^8.25.0", "@salesforce/kit": "^3.2.4", "@salesforce/sf-plugins-core": "^12.2.6", - "@salesforce/webapp-experimental": "^0.2.0", + "@salesforce/webapp-experimental": "^1.23.0", "chokidar": "^3.6.0", "http-proxy": "^1.18.1", "micromatch": "^4.0.8", @@ -78,7 +78,6 @@ "format": "wireit", "link-check": "wireit", "lint": "wireit", - "postbuild": "node scripts/copy-templates.cjs", "postpack": "sf-clean --ignore-signing-artifacts", "prepack": "sf-prepack", "prepare": "sf-install", diff --git a/scripts/copy-templates.cjs b/scripts/copy-templates.cjs deleted file mode 100644 index 7d21f0e..0000000 --- a/scripts/copy-templates.cjs +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -/* - * Cross-platform script to copy HTML templates from src to lib. - * Used by postbuild to ensure templates are available at runtime. - */ - -const fs = require('fs'); -const path = require('path'); - -const srcDir = path.join(__dirname, '..', 'src', 'templates'); -const destDir = path.join(__dirname, '..', 'lib', 'templates'); - -// Create destination directory if it doesn't exist -fs.mkdirSync(destDir, { recursive: true }); - -// Copy all HTML files -const htmlFiles = fs.readdirSync(srcDir).filter(f => f.endsWith('.html')); - -if (htmlFiles.length === 0) { - console.log('No HTML templates found to copy.'); - process.exit(0); -} - -htmlFiles.forEach(file => { - const srcPath = path.join(srcDir, file); - const destPath = path.join(destDir, file); - fs.copyFileSync(srcPath, destPath); - console.log(`Copied: ${file}`); -}); - -console.log(`Successfully copied ${htmlFiles.length} template(s) to lib/templates/`); diff --git a/src/proxy/ProxyServer.ts b/src/proxy/ProxyServer.ts index d184689..62d0af1 100644 --- a/src/proxy/ProxyServer.ts +++ b/src/proxy/ProxyServer.ts @@ -176,6 +176,11 @@ export class ProxyServer extends EventEmitter { this.server.on('connection', (socket) => { this.activeConnections.add(socket); + socket.on('error', (err) => { + // Handle ECONNRESET and other socket errors gracefully + // These can happen when the dev server crashes or a client disconnects abruptly + this.logger.debug(`Socket error (${err.message}), cleaning up connection`); + }); socket.once('close', () => { this.activeConnections.delete(socket); }); @@ -407,8 +412,6 @@ export class ProxyServer extends EventEmitter { private initializeProxyHandler(): void { const manifest: WebAppManifest = this.config.manifest ?? { name: 'webapp', - label: 'WebApp', - version: '1.0.0', outputDir: 'dist', }; diff --git a/src/server/DevServerManager.ts b/src/server/DevServerManager.ts index b302274..fa6f16b 100644 --- a/src/server/DevServerManager.ts +++ b/src/server/DevServerManager.ts @@ -430,11 +430,10 @@ export class DevServerManager extends EventEmitter { this.logger.error(`Dev server error: ${parsedError.title}`); this.logger.debug(`Error type: ${parsedError.type}`); - // Convert to SfError for proper error handling - // Use just the message (not title) since title will be shown separately - const sfError = new SfError(parsedError.message, 'DevServerError', parsedError.suggestions); - - this.emit('error', sfError); + // Emit the parsed DevServerError directly so the receiver (dev.ts) + // can access stderrLines, title, and type for the error page. + // Previously this was wrapped in SfError which lost those properties. + this.emit('error', parsedError); } // Reset state diff --git a/src/templates/ErrorPageRenderer.ts b/src/templates/ErrorPageRenderer.ts index d042ecc..a33aae7 100644 --- a/src/templates/ErrorPageRenderer.ts +++ b/src/templates/ErrorPageRenderer.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { readFileSync } from 'node:fs'; -import { join, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { getErrorPageTemplate } from '@salesforce/webapp-experimental/proxy'; import type { DevServerError } from '../config/types.js'; export type ErrorPageData = { @@ -37,55 +35,7 @@ export class ErrorPageRenderer { private template: string; public constructor() { - // Load the HTML template - const currentDir = dirname(fileURLToPath(import.meta.url)); - const templatePath = join(currentDir, 'error-page.html'); - - try { - this.template = readFileSync(templatePath, 'utf-8'); - } catch (error) { - // Log warning but don't crash - use minimal fallback template - // eslint-disable-next-line no-console - console.error(`[ErrorPageRenderer] Failed to load template from ${templatePath}:`, error); - this.template = ErrorPageRenderer.getMinimalFallbackTemplate(); - } - } - - /** - * Minimal fallback template used when the main template file cannot be loaded. - * This ensures the proxy can still display error pages even if the template is missing. - */ - private static getMinimalFallbackTemplate(): string { - return ` - - - - {{PAGE_TITLE}} - {{META_REFRESH}} - - - -
-

{{ERROR_TITLE}}

-

{{ERROR_STATUS}}

-
- {{MESSAGE_CONTENT}} -

Dev Server: {{DEV_SERVER_URL}}

-

Proxy: {{PROXY_URL}}

-

Last Check: {{LAST_CHECK_TIME}}

-
-

{{AUTO_REFRESH_TEXT}}

-
- -`; + this.template = getErrorPageTemplate(); } /** diff --git a/src/templates/error-page.html b/src/templates/error-page.html deleted file mode 100644 index 17d0699..0000000 --- a/src/templates/error-page.html +++ /dev/null @@ -1,749 +0,0 @@ - - - - - - {{PAGE_TITLE}} - Salesforce Local Dev Proxy - {{META_REFRESH}} - - - - -
- -
-
-

Local Dev Proxy

-

Salesforce preview → Proxy → Your dev server

-
-
{{ERROR_STATUS}}
-
- - -
- -
- -
-
-

{{ERROR_TITLE}}

- {{MESSAGE_CONTENT}} -
- -
{{AUTO_REFRESH_TEXT}}
- -
-

What to do next

-
    -
  • Start your dev server using: npm run dev or yarn dev
  • -
  • Verify your dev server is running on the correct port
  • -
  • Check webapplication.json for the correct dev server URL
  • -
  • This page will auto-refresh when the server is detected
  • -
-
-
- - -
-
-

⚠️ Runtime Error: {{ERROR_TYPE}}

-
-

{{ERROR_MESSAGE_TEXT}}

-
-
- -
-

Stack Trace

-
{{FORMATTED_STACK_HTML}}
-
- - - -
-

{{SUGGESTIONS_TITLE}}

-
    - {{SUGGESTIONS_LIST}} -
-
-
- - -
-
-

⚠️ {{ERROR_TITLE}}

-
-

{{ERROR_MESSAGE_TEXT}}

-
-
- -
-

Error Output

-
-
{{STDERR_OUTPUT}}
-
-
- -
{{AUTO_REFRESH_TEXT}}
- -
-

{{SUGGESTIONS_TITLE}}

-
    - {{SUGGESTIONS_LIST}} -
-
-
-
- - -
-

Diagnostics

- -
    -
  • - Dev Server URL: - {{DEV_SERVER_URL}} -
  • -
  • - Proxy URL: - {{PROXY_URL}} -
  • -
  • - Workspace Script: - {{WORKSPACE_SCRIPT}} -
  • -
  • - Target Org: - {{ORG_TARGET}} -
  • -
  • - Last Check: - {{LAST_CHECK_TIME}} -
  • - -
  • - Node Version: - {{NODE_VERSION}} -
  • -
  • - Platform: - {{PLATFORM}} -
  • -
  • - Memory Usage: - {{HEAP_USED_MB}} MB / {{HEAP_TOTAL_MB}} MB heap -
  • -
  • - Process ID: - {{PID}} -
  • -
- -
-
- [{{LAST_CHECK_TIME}}] - proxy ▶ waiting for backend... -
-
- [{{LAST_CHECK_TIME}}] - check {{DEV_SERVER_URL}} ▶ unreachable -
-
- [{{LAST_CHECK_TIME}}] - hint ▶ try "{{WORKSPACE_SCRIPT}}" -
-
- -
-
- [{{TIMESTAMP_FORMATTED}}] - error ▶ {{ERROR_TYPE}} detected -
-
- severity ▶ {{SEVERITY_LABEL}} -
-
- - -
-

⚠️ If Ctrl+C doesn't work

-

Copy and run this command in a new terminal to force-stop the proxy:

- -
-
Kill all processes on port {{PROXY_PORT}}:
-
-
lsof -ti:{{PROXY_PORT}} | xargs kill -9
- -
-
-
-
-
- - - - - -
- - diff --git a/test/commands/webapp/dev.test.ts b/test/commands/webapp/dev.test.ts index 2d320ea..3a0fcbd 100644 --- a/test/commands/webapp/dev.test.ts +++ b/test/commands/webapp/dev.test.ts @@ -29,8 +29,6 @@ describe('webapp:dev command integration', () => { it('should have correct WebAppManifest structure', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -64,8 +62,6 @@ describe('webapp:dev command integration', () => { it('should use manifest dev.url when no explicit URL', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -78,8 +74,6 @@ describe('webapp:dev command integration', () => { it('should use dev.command when no URL provided', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', @@ -94,8 +88,6 @@ describe('webapp:dev command integration', () => { it('should validate manifest with dev.url', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -104,15 +96,12 @@ describe('webapp:dev command integration', () => { // Basic validation expect(manifest.name).to.be.a('string'); - expect(manifest.version).to.match(/^\d+\.\d+\.\d+$/); expect(manifest.dev?.url).to.include('http'); }); it('should validate manifest with dev.command', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', diff --git a/test/config/ManifestWatcher.test.ts b/test/config/ManifestWatcher.test.ts index d8d7533..16d35a3 100644 --- a/test/config/ManifestWatcher.test.ts +++ b/test/config/ManifestWatcher.test.ts @@ -29,8 +29,6 @@ describe('ManifestWatcher', () => { const validManifest: WebAppManifest = { name: 'testApp', - label: 'Test Application', - version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', @@ -144,7 +142,7 @@ describe('ManifestWatcher', () => { }); describe('Partial Manifest Support', () => { - it('should accept manifest with only dev.command (no name, label, version, outputDir)', async () => { + it('should accept manifest with only dev.command (no name, outputDir)', async () => { const partialManifest = { dev: { command: 'npm run dev', @@ -160,7 +158,6 @@ describe('ManifestWatcher', () => { expect(manifest).to.exist; expect(manifest?.dev?.command).to.equal('npm run dev'); expect(manifest?.name).to.be.undefined; - expect(manifest?.label).to.be.undefined; await watcher.stop(); }); @@ -216,8 +213,6 @@ describe('ManifestWatcher', () => { const manifest = watcher.getManifest(); expect(manifest).to.exist; expect(manifest?.name).to.equal('myApp'); - expect(manifest?.label).to.be.undefined; - expect(manifest?.version).to.be.undefined; expect(manifest?.outputDir).to.be.undefined; await watcher.stop(); @@ -226,8 +221,6 @@ describe('ManifestWatcher', () => { it('should accept manifest without optional dev config', async () => { const minimalManifest = { name: 'testApp', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', }; @@ -278,13 +271,13 @@ describe('ManifestWatcher', () => { // Wait a bit then modify the file setTimeout(() => { - const updated = { ...validManifest, version: '2.0.0' }; + const updated = { ...validManifest, name: 'updatedApp' }; writeFileSync(testManifestPath, JSON.stringify(updated, null, 2)); }, 200); const event = await changePromise; expect(event.type).to.equal('changed'); - expect(event.manifest?.version).to.equal('2.0.0'); + expect(event.manifest?.name).to.equal('updatedApp'); await watcher.stop(); }); @@ -355,22 +348,22 @@ describe('ManifestWatcher', () => { // Make multiple rapid changes setTimeout(() => { - writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, version: '1.0.1' }, null, 2)); + writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, name: 'change1' }, null, 2)); }, 100); setTimeout(() => { - writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, version: '1.0.2' }, null, 2)); + writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, name: 'change2' }, null, 2)); }, 150); setTimeout(() => { - writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, version: '1.0.3' }, null, 2)); + writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, name: 'change3' }, null, 2)); }, 200); // Check that only one change event was emitted after debounce await new Promise((resolve) => setTimeout(resolve, 800)); expect(changeCount).to.equal(1); - expect(watcher.getManifest()?.version).to.equal('1.0.3'); + expect(watcher.getManifest()?.name).to.equal('change3'); await watcher.stop(); }); }); @@ -425,12 +418,12 @@ describe('ManifestWatcher', () => { eventEmitted = true; }); - writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, version: '2.0.0' }, null, 2)); + writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, name: 'changedApp' }, null, 2)); await new Promise((resolve) => setTimeout(resolve, 300)); expect(eventEmitted).to.be.false; - expect(watcher.getManifest()?.version).to.equal('1.0.0'); // Still old version + expect(watcher.getManifest()?.name).to.equal('testApp'); // Still old value await watcher.stop(); }); diff --git a/test/config/types.test.ts b/test/config/types.test.ts index 85528a8..19bdcdc 100644 --- a/test/config/types.test.ts +++ b/test/config/types.test.ts @@ -21,20 +21,16 @@ describe('TypeScript Types', () => { it('should allow valid WebAppManifest', () => { const manifest: WebAppManifest = { name: 'testApp', - label: 'Test Application', - version: '1.0.0', outputDir: 'dist', }; expect(manifest.name).to.equal('testApp'); - expect(manifest.version).to.equal('1.0.0'); + expect(manifest.outputDir).to.equal('dist'); }); it('should allow WebAppManifest with dev config', () => { const manifest: WebAppManifest = { name: 'testApp', - label: 'Test Application', - version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', @@ -46,18 +42,6 @@ describe('TypeScript Types', () => { expect(manifest.dev?.url).to.equal('http://localhost:5173'); }); - it('should allow optional description', () => { - const manifest: WebAppManifest = { - name: 'testApp', - label: 'Test Application', - description: 'This is a test app', - version: '1.0.0', - outputDir: 'dist', - }; - - expect(manifest.description).to.equal('This is a test app'); - }); - it('should allow WebAppManifest with routing config', () => { const routing: RoutingConfig = { rewrites: [{ route: '/api/:id', target: 'api/handler' }], @@ -68,8 +52,6 @@ describe('TypeScript Types', () => { const manifest: WebAppManifest = { name: 'testApp', - label: 'Test Application', - version: '1.0.0', outputDir: 'dist', routing, }; diff --git a/test/proxy/ProxyServer.test.ts b/test/proxy/ProxyServer.test.ts index 916b61f..0569cdb 100644 --- a/test/proxy/ProxyServer.test.ts +++ b/test/proxy/ProxyServer.test.ts @@ -122,8 +122,6 @@ describe('ProxyServer', () => { salesforceInstanceUrl: 'https://test.salesforce.com', manifest: { name: 'test-app', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', }, }); @@ -227,8 +225,6 @@ describe('ProxyServer', () => { salesforceInstanceUrl: 'https://test.salesforce.com', manifest: { name: 'test-app', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', }, }); @@ -236,8 +232,6 @@ describe('ProxyServer', () => { // Update manifest with routing config proxy.updateManifest({ name: 'test-app', - label: 'Test App', - version: '2.0.0', outputDir: 'dist', routing: { trailingSlash: 'always', diff --git a/yarn.lock b/yarn.lock index 6ba0d88..47bcbd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1758,6 +1758,19 @@ resolved "https://registry.npmjs.org/@salesforce/prettier-config/-/prettier-config-0.0.3.tgz" integrity sha512-hYOhoPTCSYMDYn+U1rlEk16PoBeAJPkrdg4/UtAzupM1mRRJOwEPMG1d7U8DxJFKuXW3DMEYWr2MwAIBDaHmFg== +"@salesforce/sdk-core@^1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.22.0.tgz#6488c2a64954ef554253f7d6293239d3e3ba9e61" + integrity sha512-L3GT267pg8iRJFXLUg+DVjn76UgJSwexXhWsAV5WDiLEkXlEwKdGFmpmKYbDx9M9sUN3NckiYw+trWGRjUEHNw== + +"@salesforce/sdk-data@^1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.22.0.tgz#2dbf26f8b29f4bcc56aaf070baa74dbe64d02cd6" + integrity sha512-KH5RcQfyXj0jjvpI7gv54+e7qhiOBZ+XjuBA6UsOuk4bRvRfvqtwxCl1qSTLTU6iEoburudq6ixu1n7A6MOG+g== + dependencies: + "@conduit-client/salesforce-lightning-service-worker" "^3.7.0" + "@salesforce/sdk-core" "^1.22.0" + "@salesforce/sf-plugins-core@^11.3.12": version "11.3.12" resolved "https://registry.npmjs.org/@salesforce/sf-plugins-core/-/sf-plugins-core-11.3.12.tgz" @@ -1797,13 +1810,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@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@salesforce/webapp-experimental/-/webapp-experimental-0.2.0.tgz" - integrity sha512-+E7b8u88ABJcgj7YSYiwXaF+LY9lCElibtEQbfuBdDXSUXjaMImwdZjhuyUtndIYWSXM+X38kjnIAFiikjqBIQ== +"@salesforce/webapp-experimental@^1.23.0": + version "1.23.0" + resolved "https://registry.yarnpkg.com/@salesforce/webapp-experimental/-/webapp-experimental-1.23.0.tgz#b95ebfebd3254361732e8edcfbdc56a8b819a948" + integrity sha512-5EKzZ6MFnCzmKdHSSt+28riAIgFQ+5PfPRQg3Gl0mnUA3GzN9XwzEq8hI/r52sDlKPvtb2x+MKAxyYs/182OEg== dependencies: - "@conduit-client/salesforce-lightning-service-worker" "^3.7.0" "@salesforce/core" "^8.23.4" + "@salesforce/sdk-data" "^1.22.0" axios "^1.7.7" micromatch "^4.0.8" path-to-regexp "^8.3.0" From 2e8052524be01e225830f5b36ea53f43f217941c Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 13 Feb 2026 20:11:18 +0530 Subject: [PATCH 03/15] fix: address PR feedback and merge conflicts - messages: specify 'proxy server' in Ctrl+C stop message - ProxyServer: add label and version to fallback WebAppManifest - ErrorPageRenderer: use local template (getErrorPageTemplate not in package) - tests: add label and version to WebAppManifest fixtures Co-authored-by: Cursor --- messages/webapp.dev.md | 2 +- src/proxy/ProxyServer.ts | 2 + src/templates/ErrorPageRenderer.ts | 4 +- src/templates/error-page-template.ts | 148 +++++++++++++++++++++++++++ test/commands/webapp/dev.test.ts | 10 ++ test/config/ManifestWatcher.test.ts | 2 + test/config/types.test.ts | 6 ++ test/proxy/ProxyServer.test.ts | 6 ++ 8 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/templates/error-page-template.ts diff --git a/messages/webapp.dev.md b/messages/webapp.dev.md index 6a413b1..55139c2 100644 --- a/messages/webapp.dev.md +++ b/messages/webapp.dev.md @@ -90,7 +90,7 @@ Proxy URL: %s (open this in your browser) # info.press-ctrl-c -Press Ctrl+C to stop +Press Ctrl+C to stop the proxy server # info.dev-server-healthy diff --git a/src/proxy/ProxyServer.ts b/src/proxy/ProxyServer.ts index 62d0af1..ce7a35a 100644 --- a/src/proxy/ProxyServer.ts +++ b/src/proxy/ProxyServer.ts @@ -412,6 +412,8 @@ export class ProxyServer extends EventEmitter { private initializeProxyHandler(): void { const manifest: WebAppManifest = this.config.manifest ?? { name: 'webapp', + label: 'Web App', + version: '1.0.0', outputDir: 'dist', }; diff --git a/src/templates/ErrorPageRenderer.ts b/src/templates/ErrorPageRenderer.ts index a33aae7..23777b5 100644 --- a/src/templates/ErrorPageRenderer.ts +++ b/src/templates/ErrorPageRenderer.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { getErrorPageTemplate } from '@salesforce/webapp-experimental/proxy'; import type { DevServerError } from '../config/types.js'; +import { ERROR_PAGE_TEMPLATE } from './error-page-template.js'; export type ErrorPageData = { status: string; @@ -35,7 +35,7 @@ export class ErrorPageRenderer { private template: string; public constructor() { - this.template = getErrorPageTemplate(); + this.template = ERROR_PAGE_TEMPLATE; } /** diff --git a/src/templates/error-page-template.ts b/src/templates/error-page-template.ts new file mode 100644 index 0000000..e590f5d --- /dev/null +++ b/src/templates/error-page-template.ts @@ -0,0 +1,148 @@ +/* + * 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. + */ + +/** + * HTML template for dev server error pages. + * Placeholders: {{PAGE_TITLE}}, {{META_REFRESH}}, {{ERROR_TITLE}}, {{STATUS_CLASS}}, + * {{ERROR_STATUS}}, {{MESSAGE_CONTENT}}, {{ERROR_MESSAGE_TEXT}}, {{STDERR_OUTPUT}}, + * {{SUGGESTIONS_TITLE}}, {{SUGGESTIONS_LIST}}, {{DEV_SERVER_URL}}, {{PROXY_URL}}, + * {{PROXY_PORT}}, {{ORG_TARGET}}, {{WORKSPACE_SCRIPT}}, {{LAST_CHECK_TIME}}, + * {{SIMPLE_SECTION_CLASS}}, {{RUNTIME_SECTION_CLASS}}, {{DEV_SERVER_SECTION_CLASS}}, + * {{SUGGESTIONS_SECTION_CLASS}}, {{AUTO_REFRESH_CLASS}}, {{AUTO_REFRESH_TEXT}} + */ +export const ERROR_PAGE_TEMPLATE = ` + + + + + {{PAGE_TITLE}} - Salesforce Local Dev Proxy + {{META_REFRESH}} + + + +
+
+
+

Local Dev Proxy

+

Salesforce preview → Proxy → Your dev server

+
+
{{ERROR_STATUS}}
+
+
+
+
+
+

{{ERROR_TITLE}}

+ {{MESSAGE_CONTENT}} +
+
{{AUTO_REFRESH_TEXT}}
+
+

What to do next

+
    +
  • Start your dev server using: npm run dev or yarn dev
  • +
  • Verify your dev server is running on the correct port
  • +
  • Check webapplication.json for the correct dev server URL
  • +
  • This page will auto-refresh when the server is detected
  • +
+
+
+
+
+
+

⚠️ {{ERROR_TITLE}}

+
+

{{ERROR_MESSAGE_TEXT}}

+
+
+
+

Error Output

+
+
{{STDERR_OUTPUT}}
+
+
+
{{AUTO_REFRESH_TEXT}}
+
+

{{SUGGESTIONS_TITLE}}

+
    + {{SUGGESTIONS_LIST}} +
+
+
+
+
+

Diagnostics

+
    +
  • Dev Server URL:{{DEV_SERVER_URL}}
  • +
  • Proxy URL:{{PROXY_URL}}
  • +
  • Workspace Script:{{WORKSPACE_SCRIPT}}
  • +
  • Target Org:{{ORG_TARGET}}
  • +
  • Last Check:{{LAST_CHECK_TIME}}
  • +
+
+

⚠️ If Ctrl+C doesn't work

+

Copy and run this command in a new terminal to force-stop the proxy:

+
lsof -ti:{{PROXY_PORT}} | xargs kill -9
+
+
+
+ +
+ +`; diff --git a/test/commands/webapp/dev.test.ts b/test/commands/webapp/dev.test.ts index 3a0fcbd..6bdf6de 100644 --- a/test/commands/webapp/dev.test.ts +++ b/test/commands/webapp/dev.test.ts @@ -29,6 +29,8 @@ describe('webapp:dev command integration', () => { it('should have correct WebAppManifest structure', () => { const manifest: WebAppManifest = { name: 'testWebApp', + label: 'Test Web App', + version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -62,6 +64,8 @@ describe('webapp:dev command integration', () => { it('should use manifest dev.url when no explicit URL', () => { const manifest: WebAppManifest = { name: 'testWebApp', + label: 'Test Web App', + version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -74,6 +78,8 @@ describe('webapp:dev command integration', () => { it('should use dev.command when no URL provided', () => { const manifest: WebAppManifest = { name: 'testWebApp', + label: 'Test Web App', + version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', @@ -88,6 +94,8 @@ describe('webapp:dev command integration', () => { it('should validate manifest with dev.url', () => { const manifest: WebAppManifest = { name: 'testWebApp', + label: 'Test Web App', + version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -102,6 +110,8 @@ describe('webapp:dev command integration', () => { it('should validate manifest with dev.command', () => { const manifest: WebAppManifest = { name: 'testWebApp', + label: 'Test Web App', + version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', diff --git a/test/config/ManifestWatcher.test.ts b/test/config/ManifestWatcher.test.ts index 16d35a3..447ca3f 100644 --- a/test/config/ManifestWatcher.test.ts +++ b/test/config/ManifestWatcher.test.ts @@ -29,6 +29,8 @@ describe('ManifestWatcher', () => { const validManifest: WebAppManifest = { name: 'testApp', + label: 'Test App', + version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', diff --git a/test/config/types.test.ts b/test/config/types.test.ts index 19bdcdc..a607b9d 100644 --- a/test/config/types.test.ts +++ b/test/config/types.test.ts @@ -21,6 +21,8 @@ describe('TypeScript Types', () => { it('should allow valid WebAppManifest', () => { const manifest: WebAppManifest = { name: 'testApp', + label: 'Test App', + version: '1.0.0', outputDir: 'dist', }; @@ -31,6 +33,8 @@ describe('TypeScript Types', () => { it('should allow WebAppManifest with dev config', () => { const manifest: WebAppManifest = { name: 'testApp', + label: 'Test App', + version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', @@ -52,6 +56,8 @@ describe('TypeScript Types', () => { const manifest: WebAppManifest = { name: 'testApp', + label: 'Test App', + version: '1.0.0', outputDir: 'dist', routing, }; diff --git a/test/proxy/ProxyServer.test.ts b/test/proxy/ProxyServer.test.ts index 0569cdb..a71c2ef 100644 --- a/test/proxy/ProxyServer.test.ts +++ b/test/proxy/ProxyServer.test.ts @@ -122,6 +122,8 @@ describe('ProxyServer', () => { salesforceInstanceUrl: 'https://test.salesforce.com', manifest: { name: 'test-app', + label: 'Test App', + version: '1.0.0', outputDir: 'dist', }, }); @@ -225,6 +227,8 @@ describe('ProxyServer', () => { salesforceInstanceUrl: 'https://test.salesforce.com', manifest: { name: 'test-app', + label: 'Test App', + version: '1.0.0', outputDir: 'dist', }, }); @@ -232,6 +236,8 @@ describe('ProxyServer', () => { // Update manifest with routing config proxy.updateManifest({ name: 'test-app', + label: 'Test App', + version: '1.0.0', outputDir: 'dist', routing: { trailingSlash: 'always', From d9be3cbc243a73bf309524823f5e4074dedc0cb9 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 13 Feb 2026 20:18:44 +0530 Subject: [PATCH 04/15] revert: drop merge-fix changes, keep only PR feedback Keep only the messages change: 'Press Ctrl+C to stop the proxy server'. Revert ErrorPageRenderer, ProxyServer, and test changes - will address separately. Co-authored-by: Cursor --- src/proxy/ProxyServer.ts | 2 -- src/templates/ErrorPageRenderer.ts | 4 ++-- test/commands/webapp/dev.test.ts | 10 ---------- test/config/ManifestWatcher.test.ts | 2 -- test/config/types.test.ts | 6 ------ test/proxy/ProxyServer.test.ts | 6 ------ 6 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/proxy/ProxyServer.ts b/src/proxy/ProxyServer.ts index ce7a35a..62d0af1 100644 --- a/src/proxy/ProxyServer.ts +++ b/src/proxy/ProxyServer.ts @@ -412,8 +412,6 @@ export class ProxyServer extends EventEmitter { private initializeProxyHandler(): void { const manifest: WebAppManifest = this.config.manifest ?? { name: 'webapp', - label: 'Web App', - version: '1.0.0', outputDir: 'dist', }; diff --git a/src/templates/ErrorPageRenderer.ts b/src/templates/ErrorPageRenderer.ts index 23777b5..a33aae7 100644 --- a/src/templates/ErrorPageRenderer.ts +++ b/src/templates/ErrorPageRenderer.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { getErrorPageTemplate } from '@salesforce/webapp-experimental/proxy'; import type { DevServerError } from '../config/types.js'; -import { ERROR_PAGE_TEMPLATE } from './error-page-template.js'; export type ErrorPageData = { status: string; @@ -35,7 +35,7 @@ export class ErrorPageRenderer { private template: string; public constructor() { - this.template = ERROR_PAGE_TEMPLATE; + this.template = getErrorPageTemplate(); } /** diff --git a/test/commands/webapp/dev.test.ts b/test/commands/webapp/dev.test.ts index 6bdf6de..3a0fcbd 100644 --- a/test/commands/webapp/dev.test.ts +++ b/test/commands/webapp/dev.test.ts @@ -29,8 +29,6 @@ describe('webapp:dev command integration', () => { it('should have correct WebAppManifest structure', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -64,8 +62,6 @@ describe('webapp:dev command integration', () => { it('should use manifest dev.url when no explicit URL', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -78,8 +74,6 @@ describe('webapp:dev command integration', () => { it('should use dev.command when no URL provided', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', @@ -94,8 +88,6 @@ describe('webapp:dev command integration', () => { it('should validate manifest with dev.url', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { url: 'http://localhost:5173', @@ -110,8 +102,6 @@ describe('webapp:dev command integration', () => { it('should validate manifest with dev.command', () => { const manifest: WebAppManifest = { name: 'testWebApp', - label: 'Test Web App', - version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', diff --git a/test/config/ManifestWatcher.test.ts b/test/config/ManifestWatcher.test.ts index 447ca3f..16d35a3 100644 --- a/test/config/ManifestWatcher.test.ts +++ b/test/config/ManifestWatcher.test.ts @@ -29,8 +29,6 @@ describe('ManifestWatcher', () => { const validManifest: WebAppManifest = { name: 'testApp', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', diff --git a/test/config/types.test.ts b/test/config/types.test.ts index a607b9d..19bdcdc 100644 --- a/test/config/types.test.ts +++ b/test/config/types.test.ts @@ -21,8 +21,6 @@ describe('TypeScript Types', () => { it('should allow valid WebAppManifest', () => { const manifest: WebAppManifest = { name: 'testApp', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', }; @@ -33,8 +31,6 @@ describe('TypeScript Types', () => { it('should allow WebAppManifest with dev config', () => { const manifest: WebAppManifest = { name: 'testApp', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', dev: { command: 'npm run dev', @@ -56,8 +52,6 @@ describe('TypeScript Types', () => { const manifest: WebAppManifest = { name: 'testApp', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', routing, }; diff --git a/test/proxy/ProxyServer.test.ts b/test/proxy/ProxyServer.test.ts index a71c2ef..0569cdb 100644 --- a/test/proxy/ProxyServer.test.ts +++ b/test/proxy/ProxyServer.test.ts @@ -122,8 +122,6 @@ describe('ProxyServer', () => { salesforceInstanceUrl: 'https://test.salesforce.com', manifest: { name: 'test-app', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', }, }); @@ -227,8 +225,6 @@ describe('ProxyServer', () => { salesforceInstanceUrl: 'https://test.salesforce.com', manifest: { name: 'test-app', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', }, }); @@ -236,8 +232,6 @@ describe('ProxyServer', () => { // Update manifest with routing config proxy.updateManifest({ name: 'test-app', - label: 'Test App', - version: '1.0.0', outputDir: 'dist', routing: { trailingSlash: 'always', From 593993c80f291917d10260089edd32bf19664692 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 13 Feb 2026 20:37:00 +0530 Subject: [PATCH 05/15] fix: address PR review comments - no-manifest: log applied defaults (dev command, proxy port) at info level - ready-for-development: show only URL to open in browser (proxy URL) - flags.url: add dev server URL precedence to help text Co-authored-by: Cursor --- messages/webapp.dev.md | 9 +++++---- src/commands/webapp/dev.ts | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/messages/webapp.dev.md b/messages/webapp.dev.md index 55139c2..1e70110 100644 --- a/messages/webapp.dev.md +++ b/messages/webapp.dev.md @@ -24,6 +24,8 @@ Dev server origin to forward UI/HMR/static requests 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 @@ -85,8 +87,7 @@ Proxy URL: %s (open this in your browser) # info.ready-for-development ✅ Ready for development! - → Proxy: %s (open this in your browser) - → Dev server: %s + → %s (open this URL in your browser) # info.press-ctrl-c @@ -153,9 +154,9 @@ Auto-selected webapp "%s" (running from inside its folder) Select the webapp to run: -# warning.no-manifest +# info.no-manifest-defaults -No webapplication.json found - using defaults (command: %s) +No webapplication.json found. Using defaults: dev command=%s, proxy port=%s Tip: See "sf webapp dev --help" for configuration options. diff --git a/src/commands/webapp/dev.ts b/src/commands/webapp/dev.ts index 6428f77..fba519e 100644 --- a/src/commands/webapp/dev.ts +++ b/src/commands/webapp/dev.ts @@ -213,8 +213,8 @@ export default class WebappDev extends SfCommand { this.warn(messages.getMessage('error.manifest-watch-failed', [error.message])); }); } else { - // No manifest - show warning and use defaults - this.warn(messages.getMessage('warning.no-manifest', [DEFAULT_DEV_COMMAND])); + // No manifest - log applied defaults for troubleshooting + this.log(messages.getMessage('info.no-manifest-defaults', [DEFAULT_DEV_COMMAND, String(flags.port)])); this.log(''); this.log(messages.getMessage('info.starting-webapp', [selectedWebapp.name])); } @@ -370,7 +370,7 @@ export default class WebappDev extends SfCommand { // Display usage instructions this.log(''); - this.log(messages.getMessage('info.ready-for-development', [proxyUrl, devServerUrl ?? 'N/A'])); + this.log(messages.getMessage('info.ready-for-development', [proxyUrl])); this.log(messages.getMessage('info.press-ctrl-c')); this.log(''); From 49c4034fdc2dc1665f1d6f95f12bf16de6da3f42 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 13 Feb 2026 20:55:26 +0530 Subject: [PATCH 06/15] chore: remove unused error-page-template.ts Co-authored-by: Cursor --- src/templates/error-page-template.ts | 148 --------------------------- 1 file changed, 148 deletions(-) delete mode 100644 src/templates/error-page-template.ts diff --git a/src/templates/error-page-template.ts b/src/templates/error-page-template.ts deleted file mode 100644 index e590f5d..0000000 --- a/src/templates/error-page-template.ts +++ /dev/null @@ -1,148 +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. - */ - -/** - * HTML template for dev server error pages. - * Placeholders: {{PAGE_TITLE}}, {{META_REFRESH}}, {{ERROR_TITLE}}, {{STATUS_CLASS}}, - * {{ERROR_STATUS}}, {{MESSAGE_CONTENT}}, {{ERROR_MESSAGE_TEXT}}, {{STDERR_OUTPUT}}, - * {{SUGGESTIONS_TITLE}}, {{SUGGESTIONS_LIST}}, {{DEV_SERVER_URL}}, {{PROXY_URL}}, - * {{PROXY_PORT}}, {{ORG_TARGET}}, {{WORKSPACE_SCRIPT}}, {{LAST_CHECK_TIME}}, - * {{SIMPLE_SECTION_CLASS}}, {{RUNTIME_SECTION_CLASS}}, {{DEV_SERVER_SECTION_CLASS}}, - * {{SUGGESTIONS_SECTION_CLASS}}, {{AUTO_REFRESH_CLASS}}, {{AUTO_REFRESH_TEXT}} - */ -export const ERROR_PAGE_TEMPLATE = ` - - - - - {{PAGE_TITLE}} - Salesforce Local Dev Proxy - {{META_REFRESH}} - - - -
-
-
-

Local Dev Proxy

-

Salesforce preview → Proxy → Your dev server

-
-
{{ERROR_STATUS}}
-
-
-
-
-
-

{{ERROR_TITLE}}

- {{MESSAGE_CONTENT}} -
-
{{AUTO_REFRESH_TEXT}}
-
-

What to do next

-
    -
  • Start your dev server using: npm run dev or yarn dev
  • -
  • Verify your dev server is running on the correct port
  • -
  • Check webapplication.json for the correct dev server URL
  • -
  • This page will auto-refresh when the server is detected
  • -
-
-
-
-
-
-

⚠️ {{ERROR_TITLE}}

-
-

{{ERROR_MESSAGE_TEXT}}

-
-
-
-

Error Output

-
-
{{STDERR_OUTPUT}}
-
-
-
{{AUTO_REFRESH_TEXT}}
-
-

{{SUGGESTIONS_TITLE}}

-
    - {{SUGGESTIONS_LIST}} -
-
-
-
-
-

Diagnostics

-
    -
  • Dev Server URL:{{DEV_SERVER_URL}}
  • -
  • Proxy URL:{{PROXY_URL}}
  • -
  • Workspace Script:{{WORKSPACE_SCRIPT}}
  • -
  • Target Org:{{ORG_TARGET}}
  • -
  • Last Check:{{LAST_CHECK_TIME}}
  • -
-
-

⚠️ If Ctrl+C doesn't work

-

Copy and run this command in a new terminal to force-stop the proxy:

-
lsof -ti:{{PROXY_PORT}} | xargs kill -9
-
-
-
- -
- -`; From ea13492312da03e1a4ecbd56d05e658430dbb0dd Mon Sep 17 00:00:00 2001 From: deepu-mungamuri94 <36835750+deepu-mungamuri94@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:48:30 +0530 Subject: [PATCH 07/15] @W-21111861@ feat: Skip standalone proxy when Vite WebApp proxy is active (#23) * feat: add Vite proxy detection and TTY-aware messaging - Detect Vite WebApp proxy via health check, skip standalone proxy when active - Use getErrorPageTemplate from @salesforce/webapp-experimental/proxy - TTY-aware stop message (Ctrl+C in terminal vs VS Code command palette) - Add info.ready-for-development-vite for Vite proxy case - Simplify ready-for-development to show only URL to open - Resolve PR review comments on messaging Co-authored-by: Cursor * fix: remove label/version to match @salesforce/webapp-experimental 1.x type - Run yarn install to resolve 1.23.0+ (was 0.2.0 from stale lock) - ProxyServer: use minimal fallback { name, outputDir } per PR #22 - Tests: remove label/version from manifest fixtures - ManifestWatcher tests: fix assertions for simplified type Co-authored-by: Cursor * fix: group ready-for-development messages, simplify Vite message format Co-authored-by: Cursor * fix: update server-running message - remove bold, add quotes Co-authored-by: Cursor --------- Co-authored-by: Cursor --- README.md | 474 +++++++++++++++++++++++----- messages/webapp.dev.md | 15 +- package.json | 2 +- src/commands/webapp/dev.ts | 111 +++++-- src/proxy/ProxyServer.ts | 5 - src/server/DevServerManager.ts | 18 +- test/commands/webapp/dev.test.ts | 146 +++++++++ test/config/ManifestWatcher.test.ts | 19 +- yarn.lock | 34 +- 9 files changed, 671 insertions(+), 153 deletions(-) diff --git a/README.md b/README.md index f1cdd12..efd03b4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![NPM](https://img.shields.io/npm/v/@salesforce/plugin-app-dev.svg?label=@salesforce/plugin-app-dev)](https://www.npmjs.com/package/@salesforce/plugin-app-dev) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-app-dev.svg)](https://npmjs.org/package/@salesforce/plugin-app-dev) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) -# Salesforce CLI App Dev Plugin +# Salesforce CLI Webapp 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. @@ -10,14 +10,18 @@ This plugin is bundled with the [Salesforce CLI](https://developer.salesforce.co We always recommend using the latest version of these commands bundled with the CLI, however, you can install a specific version or tag if needed. -## Features +## Key Features -- 🔐 **Local Development Proxy** - Run webapps 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 +- **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 to select webapp at project root +- **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 +- **Manifest Hot Reload**: Edit `webapplication.json` while running - changes apply automatically +- **Health Monitoring**: Displays helpful error pages when dev server is down with auto-refresh +- **Framework Agnostic**: Works with any web framework (React, Vue, Angular, etc.) ## Quick Start @@ -72,118 +76,424 @@ We always recommend using the latest version of these commands bundled with the sf plugins install @salesforce/plugin-app-dev@x.y.z ``` -## Issues +--- -Please report any issues at https://github.com/forcedotcom/cli/issues +## Quick Start -## Contributing +### 1. Create your webapp in the SFDX project structure -1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md) -2. Create a new issue before starting your project so that we can keep track of - what you are trying to add/fix. That way, we can also offer suggestions or - let you know if there is already an effort in progress. -3. Fork this repository. -4. [Build the plugin locally](#build) -5. Create a _topic_ branch in your fork. Note, this step is recommended but technically not required if contributing using a fork. -6. Edit the code in your fork. -7. Write appropriate tests for your changes. Try to achieve at least 95% code coverage on any new code. No pull request will be accepted without unit tests. -8. Sign CLA (see [CLA](#cla) below). -9. Send us a pull request when you are done. We'll review your code, suggest any needed changes, and merge it in. +``` +my-sfdx-project/ +├── sfdx-project.json +└── force-app/main/default/webapplications/ + └── my-app/ + ├── my-app.webapplication-meta.xml # Required: identifies as webapp + ├── package.json + ├── src/ + └── webapplication.json # Optional: dev configuration +``` -### CLA +### 2. Run the command -External contributors will be required to sign a Contributor's License -Agreement. You can do so by going to https://cla.salesforce.com/sign-cla. +```bash +sf webapp dev --target-org myOrg --open +``` -### Build +### 3. Start developing + +Browser opens with your app running and Salesforce authentication ready. + +- **With Vite plugin**: Open `http://localhost:5173` (Vite handles proxy) +- **Without Vite plugin**: Open `http://localhost:4545` (standalone proxy) + +> **Note**: `{name}.webapplication-meta.xml` is **required** to identify a valid webapp. The `webapplication.json` is optional - if not present, defaults to `npm run dev` command. + +--- -To build the plugin locally, make sure to have yarn installed and run the following commands: +## Commands + +### `sf webapp dev` + +Start a local development proxy server for webapp development with Salesforce authentication. ```bash -# Clone the repository -git clone git@github.com:salesforcecli/plugin-app-dev +sf webapp dev [OPTIONS] +``` + +#### Options + +| Option | Short | Description | Default | +| -------------- | ----- | ----------------------------------------------- | ------------- | +| `--target-org` | `-o` | Salesforce org alias or username | Required | +| `--name` | `-n` | Web application name (from webapplication.json) | 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 webapp +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 proxy port +sf webapp dev --target-org myOrg --port 8080 + +# Connect to existing dev server (proxy-only mode) +sf webapp dev --target-org myOrg --url http://localhost:5173 + +# Debug mode +SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg +``` + +--- + +## Configuration + +### webapplication.json Schema + +The `webapplication.json` file is **optional**. If not present, defaults are used. + +| Field | Type | Description | Default | +| ------------- | ------ | ----------------------------------------- | ------------- | +| `name` | string | Unique identifier (used with --name flag) | Folder name | +| `dev.command` | string | Command to start the dev server | `npm run dev` | +| `dev.url` | string | Dev server URL (skip starting server) | Auto-detect | + +#### Examples + +**No manifest (uses defaults):** +``` +webapplications/my-app/ +├── my-app.webapplication-meta.xml +├── package.json # Has "scripts": { "dev": "vite" } +└── src/ +``` + +**Custom dev command:** +```json +{ + "dev": { + "command": "npm start" + } +} +``` + +**Explicit URL (dev server already running):** +```json +{ + "dev": { + "url": "http://localhost:5173" + } +} +``` + +--- + +## Webapp Discovery + +The command discovers webapps using a deterministic algorithm. Webapps are identified by the presence of a `{name}.webapplication-meta.xml` file (SFDX metadata format). + +### 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 | 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 -# Install the dependencies and compile -yarn && yarn build ``` +my-sfdx-project/ +├── sfdx-project.json # SFDX project marker +└── force-app/main/default/ + └── webapplications/ # Standard SFDX location + ├── app-one/ + │ ├── app-one.webapplication-meta.xml # Required + │ ├── webapplication.json # Optional + │ ├── package.json + │ └── src/ + └── app-two/ + ├── app-two.webapplication-meta.xml # Required + ├── package.json + └── src/ +``` + +### Interactive Selection + +When at the SFDX project root, you'll see an interactive prompt to select a webapp: + +``` +? Select the webapp to run: (Use arrow keys) +❯ MyApp + app-two + CustomName +``` + +--- + +## Vite Integration (Recommended) + +When using **Vite** as your bundler, the `@salesforce/vite-plugin-webapp-experimental` package provides built-in proxy functionality. + +### Setup -To use your plugin, run using the local `./bin/dev` or `./bin/dev.cmd` file. +**1. Install the Vite plugin** ```bash -# Run using local run file. -./bin/dev hello world +npm install -D @salesforce/vite-plugin-webapp-experimental ``` -There should be no differences when running via the Salesforce CLI or using the local run file. However, it can be useful to link the plugin to do some additional testing or run your commands from anywhere on your machine. +**2. Configure vite.config.ts** + +```typescript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import salesforce from '@salesforce/vite-plugin-webapp-experimental'; + +export default defineConfig({ + plugins: [ + react(), + salesforce() // No configuration needed + ], +}); +``` + +**3. Run the dev command** ```bash -# Link your plugin to the sf cli -sf plugins link . -# To verify -sf plugins +sf webapp dev --target-org myOrg ``` -## Commands +### How It Works -### `sf webapp dev` +The CLI automatically detects whether your Vite dev server has the Salesforce plugin by sending a health check request. If the plugin responds with `X-Salesforce-WebApp-Proxy: true`, the CLI skips starting its standalone proxy. -Start a local development proxy server for webapp development with Salesforce authentication. +| Scenario | Proxy Behavior | +|-----------------------------|------------------------------------------------| +| Vite plugin **present** | Uses Vite's built-in proxy (open `:5173`) | +| Vite plugin **not present** | CLI creates standalone proxy (open `:4545`) | + +### Benefits + +| Feature | Vite Plugin | Standalone Proxy | +|-------------------------|--------------------|---------------------------| +| Single port to access | ✅ (5173) | ❌ (proxy 4545, dev 5173) | +| Simpler browser URL | ✅ `localhost:5173`| `localhost:4545` | +| HMR through same port | ✅ Native | ✅ Forwarded | + +--- + +## The `--url` Flag + +The `--url` flag provides control over which dev server URL the proxy uses. + +| Scenario | What Happens | +| ------------------------ | ----------------------------------------------------------------- | +| `--url` is reachable | **Proxy-only mode**: Skips starting dev server, only starts proxy | +| `--url` is NOT reachable | Starts dev server, warns if actual URL differs from `--url` | +| No `--url` provided | Starts dev server automatically, detects URL | + +### Example: Connect to Existing Dev Server + +```bash +# Terminal 1: Start your dev server manually +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 +``` + +--- + +## Troubleshooting + +### "No webapp found" or "No valid webapps" + +Ensure your webapp has the required `.webapplication-meta.xml` file: + +``` +webapplications/my-app/ +├── my-app.webapplication-meta.xml # Required! +├── package.json +└── webapplication.json # Optional +``` + +### "You are inside webapp X but specified --name Y" + +**Solutions:** +- Remove `--name` to use the current webapp +- Navigate to the project root and use `--name` + +### "Dependencies Not Installed" / "command not found" + +```bash +cd webapplications/my-app +npm install +``` + +### "Port 4545 already in use" ```bash -USAGE - $ sf webapp dev --name --target-org [options] +sf webapp dev --port 8080 --target-org myOrg +``` -REQUIRED FLAGS - -n, --name= Name of the webapp (must match webapplication.json) - -o, --target-org= Salesforce org to authenticate against +### "Authentication Failed" -OPTIONAL FLAGS - -u, --url= Dev server URL (overrides webapplication.json) - -p, --port= Proxy server port (default: 4545) - --open Open browser automatically +```bash +sf org login web --alias myOrg +``` -GLOBAL FLAGS - --flags-dir= Import flag values from a directory - --json Format output as json +### Debug Mode -DESCRIPTION - Start a local development proxy server for webapp development. +```bash +# Terminal 1: Tail logs +tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev - This command starts a local HTTP proxy server that handles Salesforce - authentication and routes requests between your local dev server and - Salesforce APIs. It automatically spawns and monitors your dev server, - detects the URL, and provides health monitoring. +# Terminal 2: Run with debug +SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg +``` -EXAMPLES - Start proxy with automatic dev server management: +--- + +## Architecture + +### Request Flow + +The command supports two proxy modes: + +**With Vite Plugin:** +``` +Browser → Vite Dev Server (:5173) → Salesforce (with auth) + ↓ + Proxy handles: + • /services/* → Salesforce + • Everything else → Vite HMR +``` + +**Standalone Proxy:** +``` +Browser → Proxy Server (:4545) → Salesforce (with auth) + ↓ + Dev Server (:5173) for static assets +``` - $ sf webapp dev --name myapp --target-org myorg --open +### Request Routing - Use existing dev server: +| URL Path | Routed To | +|-----------------------------|---------------------| +| `/services/*`, `/lwr/apex/*`| Salesforce (+ auth) | +| Everything else | Dev Server | - $ sf webapp dev --name myapp --target-org myorg --url http://localhost:5173 --open +--- - Use custom proxy port: +## VSCode Integration - $ sf webapp dev --name myapp --target-org myorg --port 8080 --open +The command integrates with the Salesforce VSCode UI Preview extension (`salesforcedx-vscode-ui-preview`): -SUPPORTED DEV SERVERS - - Vite - - Create React App (Webpack) - - Next.js - - Any server that outputs http://localhost:PORT +1. Extension detects `webapplication.json` in workspace +2. User clicks "Preview" button +3. Extension executes: `sf webapp dev --target-org --open` +4. Browser opens with the app running -FEATURES - - Automatic Salesforce authentication injection - - Intelligent request routing (Salesforce vs dev server) - - WebSocket support for Hot Module Replacement (HMR) - - Beautiful HTML error pages with auto-refresh - - Periodic health monitoring (every 5s) - - Configuration file watching (webapplication.json) - - Graceful shutdown on Ctrl+C +--- -SEE ALSO - - Complete Guide: SF_WEBAPP_DEV_GUIDE.md +## JSON Output + +For scripting and CI/CD: + +```bash +sf webapp dev --target-org myOrg --json +``` + +```json +{ + "status": 0, + "result": { + "url": "http://localhost:4545", + "devServerUrl": "http://localhost:5173" + } +} +``` + +--- + +## Issues + +Please report any issues at https://github.com/forcedotcom/cli/issues + +## Contributing + +1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md) +2. Create a new issue before starting your project so that we can keep track of what you are trying to add/fix. +3. Fork this repository. +4. [Build the plugin locally](#build) +5. Create a _topic_ branch in your fork. +6. Edit the code in your fork. +7. Write appropriate tests for your changes. Try to achieve at least 95% code coverage on any new code. +8. Sign CLA (see [CLA](#cla) below). +9. Send us a pull request when you are done. + +### CLA + +External contributors will be required to sign a Contributor's License Agreement. You can do so by going to https://cla.salesforce.com/sign-cla. + +### Build + +```bash +# Clone the repository +git clone git@github.com:salesforcecli/plugin-webapp + +# Install dependencies and compile +yarn && yarn build + +# Run using local dev file +./bin/dev webapp dev --target-org myOrg + +# Link to SF CLI for testing +sf plugins link . +sf plugins # Verify + +# After code changes, just rebuild +yarn build +``` + +### Project Structure + +``` +plugin-webapp/ +├── src/ +│ ├── commands/webapp/ +│ │ └── 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 +│ ├── server/ +│ │ └── DevServerManager.ts # Dev server process management +│ ├── error/ +│ │ └── DevServerErrorParser.ts # Parse dev server errors +│ └── templates/ +│ ├── ErrorPageRenderer.ts # Browser error page generation +│ └── error-page.html # Error page HTML template +├── messages/ +│ └── webapp.dev.md # CLI messages and help text +└── schemas/ + └── webapp-dev.json # JSON schema for output ``` diff --git a/messages/webapp.dev.md b/messages/webapp.dev.md index 1e70110..a27d832 100644 --- a/messages/webapp.dev.md +++ b/messages/webapp.dev.md @@ -82,17 +82,26 @@ Dev server URL: %s # info.proxy-url -Proxy URL: %s (open this in your browser) +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 the proxy server +# info.server-running + +Dev server is running. Stop it by running "SFDX: Close Live Preview" from the VS Code command palette. + # info.dev-server-healthy ✓ Dev server is responding at: %s @@ -178,3 +187,7 @@ Using default dev command: %s ⚠️ 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 9185089..292b35f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@oclif/plugin-command-snapshot": "^5.3.8", "@salesforce/cli-plugins-testkit": "^5.3.41", "@salesforce/dev-scripts": "^11.0.4", - "@salesforce/plugin-command-reference": "^3.1.79", + "@salesforce/plugin-command-reference": "^3.1.77", "@types/http-proxy": "^1.17.14", "@types/micromatch": "^4.0.10", "eslint-plugin-sf-plugin": "^1.20.33", diff --git a/src/commands/webapp/dev.ts b/src/commands/webapp/dev.ts index a6f784d..0959e4f 100644 --- a/src/commands/webapp/dev.ts +++ b/src/commands/webapp/dev.ts @@ -118,6 +118,30 @@ export default class WebappDev extends SfCommand { } } + /** + * Check if Vite's WebAppProxyHandler 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. + * + * @param devServerUrl - The dev server URL to check + * @returns true if Vite's proxy is handling requests, false otherwise + */ + private static async checkViteProxyActive(devServerUrl: string): Promise { + try { + // The Vite plugin uses a query parameter for health checks, not a path + const healthUrl = new URL(devServerUrl); + healthUrl.searchParams.set('sfProxyHealthCheck', 'true'); + const response = await fetch(healthUrl.toString(), { + method: 'GET', + signal: AbortSignal.timeout(3000), // 3 second timeout + }); + return response.headers.get('X-Salesforce-WebApp-Proxy') === 'true'; + } catch { + // Health check failed - Vite proxy not active + return false; + } + } + // eslint-disable-next-line complexity public async run(): Promise { const { flags } = await this.parse(WebappDev); @@ -290,7 +314,7 @@ export default class WebappDev extends SfCommand { const actualDevServerUrl = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject( - new SfError('Dev server did not start within 30 seconds.', 'DevServerTimeoutError', [ + new SfError('❌ Dev server did not start within 30 seconds.', 'DevServerTimeoutError', [ 'The dev server may be taking longer than expected to start', 'Check if the dev server command is correct in webapplication.json', 'Try running the dev server command manually to see if it starts', @@ -327,51 +351,78 @@ 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 webapplication.json.', 'DevServerUrlError' ); } - // Step 5: Start proxy server - this.logger.debug(`Starting proxy server on port ${flags.port}...`); - const salesforceInstanceUrl = orgConnection.instanceUrl; - this.proxyServer = new ProxyServer({ - devServerUrl, - salesforceInstanceUrl, - port: flags.port, - manifest: manifest ?? undefined, - orgAlias: orgUsername, - }); + // 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); - await this.proxyServer.start(); - const proxyUrl = this.proxyServer.getProxyUrl(); - this.logger.debug(`Proxy server running on ${proxyUrl}`); + // Track the final URL to open in browser (either proxy or dev server) + let finalUrl: string; - // Listen for dev server status changes (minimal output) - this.proxyServer.on('dev-server-up', (url: string) => { - this.logger?.debug(messages.getMessage('info.dev-server-detected', [url])); - }); + if (viteProxyActive) { + // Vite's WebAppProxyHandler 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; + } else { + // Start standalone proxy server + this.logger.debug(`Starting proxy server on port ${flags.port}...`); + const salesforceInstanceUrl = orgConnection.instanceUrl; + this.proxyServer = new ProxyServer({ + devServerUrl, + salesforceInstanceUrl, + port: flags.port, + manifest: manifest ?? undefined, + orgAlias: orgUsername, + }); - this.proxyServer.on('dev-server-down', (url: string) => { - this.log(messages.getMessage('warning.dev-server-unreachable-status', [url])); - this.log(messages.getMessage('info.start-dev-server-hint')); - }); + await this.proxyServer.start(); + const proxyUrl = this.proxyServer.getProxyUrl(); + this.logger.debug(`Proxy server running on ${proxyUrl}`); + + // Listen for dev server status changes (minimal output) + this.proxyServer.on('dev-server-up', (url: string) => { + this.logger?.debug(messages.getMessage('info.dev-server-detected', [url])); + }); + + this.proxyServer.on('dev-server-down', (url: string) => { + this.log(messages.getMessage('warning.dev-server-unreachable-status', [url])); + this.log(messages.getMessage('info.start-dev-server-hint')); + }); - // Step 6: Check if dev server is reachable (non-blocking warning) - if (devServerUrl) { + finalUrl = proxyUrl; + } + + // Step 6: Check if dev server is reachable (non-blocking warning) - only when using standalone proxy + if (!viteProxyActive && devServerUrl) { await this.checkDevServerHealth(devServerUrl); } // Step 7: Open browser if requested if (flags.open) { this.logger.debug('Opening browser...'); - await WebappDev.openBrowser(proxyUrl); + await WebappDev.openBrowser(finalUrl); } // Display usage instructions this.log(''); - this.log(messages.getMessage('info.ready-for-development', [proxyUrl])); - this.log(messages.getMessage('info.press-ctrl-c')); + if (viteProxyActive) { + this.log(messages.getMessage('info.ready-for-development-vite', [devServerUrl])); + } else { + this.log(messages.getMessage('info.ready-for-development', [finalUrl])); + } + // Show appropriate stop message based on execution context + // In TTY (interactive terminal): show "Press Ctrl+C to stop" + // In non-TTY (IDE, CI, piped): show generic "Server running" message + if (process.stdout.isTTY) { + this.log(messages.getMessage('info.press-ctrl-c')); + } else { + this.log(messages.getMessage('info.server-running')); + } this.log(''); // Keep the command running until interrupted or dev server exits @@ -403,7 +454,7 @@ export default class WebappDev extends SfCommand { // Return result (never reached, but required for type safety) return { - url: proxyUrl, + url: finalUrl, devServerUrl: devServerUrl ?? '', }; } catch (error) { @@ -417,7 +468,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 webapp 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/proxy/ProxyServer.ts b/src/proxy/ProxyServer.ts index 62d0af1..8e3f977 100644 --- a/src/proxy/ProxyServer.ts +++ b/src/proxy/ProxyServer.ts @@ -176,11 +176,6 @@ export class ProxyServer extends EventEmitter { this.server.on('connection', (socket) => { this.activeConnections.add(socket); - socket.on('error', (err) => { - // Handle ECONNRESET and other socket errors gracefully - // These can happen when the dev server crashes or a client disconnects abruptly - this.logger.debug(`Socket error (${err.message}), cleaning up connection`); - }); socket.once('close', () => { this.activeConnections.delete(socket); }); diff --git a/src/server/DevServerManager.ts b/src/server/DevServerManager.ts index fa6f16b..6475d0d 100644 --- a/src/server/DevServerManager.ts +++ b/src/server/DevServerManager.ts @@ -210,7 +210,7 @@ export class DevServerManager extends EventEmitter { // Validate that command is provided if (!this.options.command) { throw new SfError( - 'Dev server command is required when explicit URL is not provided', + '❌ Dev server command is required when explicit URL is not provided', 'DevServerCommandRequired', ['Provide a "command" in DevServerOptions', 'Or provide an "explicitUrl" to skip spawning'] ); @@ -232,7 +232,7 @@ export class DevServerManager extends EventEmitter { } catch (error) { const sfError = error instanceof Error ? error : new Error(error instanceof Object ? JSON.stringify(error) : String(error)); - throw new SfError(`Failed to spawn dev server process: ${sfError.message}`, 'DevServerSpawnError', [ + throw new SfError(`❌ Failed to spawn dev server process: ${sfError.message}`, 'DevServerSpawnError', [ `Verify the command is correct: ${this.options.command}`, 'Check that the executable exists in your PATH', 'Ensure you have the necessary dependencies installed', @@ -430,10 +430,12 @@ export class DevServerManager extends EventEmitter { this.logger.error(`Dev server error: ${parsedError.title}`); this.logger.debug(`Error type: ${parsedError.type}`); - // Emit the parsed DevServerError directly so the receiver (dev.ts) - // can access stderrLines, title, and type for the error page. - // Previously this was wrapped in SfError which lost those properties. - this.emit('error', parsedError); + // Convert to SfError for proper error handling + // Use just the message (not title) since title will be shown separately + // Prefix with ❌ for visual consistency with success messages (✅) + const sfError = new SfError(`❌ ${parsedError.message}`, 'DevServerError', parsedError.suggestions); + + this.emit('error', sfError); } // Reset state @@ -450,7 +452,7 @@ export class DevServerManager extends EventEmitter { private handleProcessError(error: Error): void { this.logger.error(`Dev server process error: ${error.message}`); - const sfError = new SfError(`Dev server process error: ${error.message}`, 'DevServerProcessError', [ + const sfError = new SfError(`❌ Dev server process error: ${error.message}`, 'DevServerProcessError', [ 'Check that the command is correct in webapplication.json', 'Verify all dependencies are installed', 'Try running the command manually to see the error', @@ -469,7 +471,7 @@ export class DevServerManager extends EventEmitter { this.logger.error('Dev server failed to start within timeout period'); const error = new SfError( - `Dev server did not start within ${this.options.startupTimeout / 1000} seconds`, + `❌ Dev server did not start within ${this.options.startupTimeout / 1000} seconds`, 'DevServerStartupTimeout', [ 'The dev server may be taking longer than expected to start', diff --git a/test/commands/webapp/dev.test.ts b/test/commands/webapp/dev.test.ts index 3a0fcbd..161b7f5 100644 --- a/test/commands/webapp/dev.test.ts +++ b/test/commands/webapp/dev.test.ts @@ -15,6 +15,7 @@ */ import { expect } from 'chai'; +import sinon from 'sinon'; import { TestContext } from '@salesforce/core/testSetup'; import type { WebAppManifest, WebAppDevResult } from '../../../src/config/types.js'; @@ -25,6 +26,151 @@ describe('webapp:dev command integration', () => { $$.restore(); }); + describe('Vite Proxy Detection', () => { + let fetchStub: sinon.SinonStub; + + beforeEach(() => { + fetchStub = sinon.stub(global, 'fetch'); + }); + + afterEach(() => { + fetchStub.restore(); + }); + + /** + * Helper function that mirrors the checkViteProxyActive logic from dev.ts + * This allows us to test the detection behavior without needing to run the full command + */ + async function checkViteProxyActive(devServerUrl: string): Promise { + try { + const healthUrl = new URL(devServerUrl); + healthUrl.searchParams.set('sfProxyHealthCheck', 'true'); + const response = await fetch(healthUrl.toString(), { + method: 'GET', + signal: AbortSignal.timeout(3000), + }); + return response.headers.get('X-Salesforce-WebApp-Proxy') === 'true'; + } catch { + return false; + } + } + + it('should return true when X-Salesforce-WebApp-Proxy header is present and true', async () => { + const mockHeaders = new Headers(); + mockHeaders.set('X-Salesforce-WebApp-Proxy', 'true'); + + fetchStub.resolves({ + ok: true, + headers: mockHeaders, + } as Response); + + const result = await checkViteProxyActive('http://localhost:5173'); + + expect(result).to.be.true; + expect(fetchStub.calledOnce).to.be.true; + + // Verify the correct URL with query parameter was called + const calledUrl = fetchStub.firstCall.args[0] as string; + expect(calledUrl).to.include('sfProxyHealthCheck=true'); + }); + + it('should return false when X-Salesforce-WebApp-Proxy header is not present', async () => { + const mockHeaders = new Headers(); + // No X-Salesforce-WebApp-Proxy header + + fetchStub.resolves({ + ok: true, + headers: mockHeaders, + } as Response); + + const result = await checkViteProxyActive('http://localhost:5173'); + + expect(result).to.be.false; + }); + + it('should return false when X-Salesforce-WebApp-Proxy header is present but not "true"', async () => { + const mockHeaders = new Headers(); + mockHeaders.set('X-Salesforce-WebApp-Proxy', 'false'); + + fetchStub.resolves({ + ok: true, + headers: mockHeaders, + } as Response); + + const result = await checkViteProxyActive('http://localhost:5173'); + + expect(result).to.be.false; + }); + + it('should return false when fetch throws an error (network failure)', async () => { + fetchStub.rejects(new Error('Network error')); + + const result = await checkViteProxyActive('http://localhost:5173'); + + expect(result).to.be.false; + }); + + it('should return false when fetch times out', async () => { + fetchStub.rejects(new DOMException('The operation was aborted', 'AbortError')); + + const result = await checkViteProxyActive('http://localhost:5173'); + + expect(result).to.be.false; + }); + + it('should return false when dev server is not reachable (connection refused)', async () => { + fetchStub.rejects(new TypeError('Failed to fetch')); + + const result = await checkViteProxyActive('http://localhost:5173'); + + expect(result).to.be.false; + }); + + it('should construct correct health check URL with query parameter', async () => { + const mockHeaders = new Headers(); + mockHeaders.set('X-Salesforce-WebApp-Proxy', 'true'); + + fetchStub.resolves({ + ok: true, + headers: mockHeaders, + } as Response); + + await checkViteProxyActive('http://localhost:5173'); + + const calledUrl = fetchStub.firstCall.args[0] as string; + expect(calledUrl).to.equal('http://localhost:5173/?sfProxyHealthCheck=true'); + }); + + it('should preserve existing query parameters when adding health check', async () => { + const mockHeaders = new Headers(); + mockHeaders.set('X-Salesforce-WebApp-Proxy', 'true'); + + fetchStub.resolves({ + ok: true, + headers: mockHeaders, + } as Response); + + await checkViteProxyActive('http://localhost:5173/?existing=param'); + + const calledUrl = fetchStub.firstCall.args[0] as string; + expect(calledUrl).to.include('existing=param'); + expect(calledUrl).to.include('sfProxyHealthCheck=true'); + }); + + it('should use GET method for health check request', async () => { + const mockHeaders = new Headers(); + fetchStub.resolves({ + ok: true, + headers: mockHeaders, + } as Response); + + await checkViteProxyActive('http://localhost:5173'); + + const options = fetchStub.firstCall.args[1] as RequestInit; + expect(options.method).to.equal('GET'); + }); + }); + describe('Type Definitions', () => { it('should have correct WebAppManifest structure', () => { const manifest: WebAppManifest = { diff --git a/test/config/ManifestWatcher.test.ts b/test/config/ManifestWatcher.test.ts index 16d35a3..54d72a2 100644 --- a/test/config/ManifestWatcher.test.ts +++ b/test/config/ManifestWatcher.test.ts @@ -142,7 +142,7 @@ describe('ManifestWatcher', () => { }); describe('Partial Manifest Support', () => { - it('should accept manifest with only dev.command (no name, outputDir)', async () => { + it('should accept manifest with only dev.command (no name, label, version, outputDir)', async () => { const partialManifest = { dev: { command: 'npm run dev', @@ -271,13 +271,13 @@ describe('ManifestWatcher', () => { // Wait a bit then modify the file setTimeout(() => { - const updated = { ...validManifest, name: 'updatedApp' }; + const updated = { ...validManifest, version: '2.0.0' }; writeFileSync(testManifestPath, JSON.stringify(updated, null, 2)); }, 200); const event = await changePromise; expect(event.type).to.equal('changed'); - expect(event.manifest?.name).to.equal('updatedApp'); + expect((event.manifest as unknown as Record)?.version).to.equal('2.0.0'); await watcher.stop(); }); @@ -348,22 +348,22 @@ describe('ManifestWatcher', () => { // Make multiple rapid changes setTimeout(() => { - writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, name: 'change1' }, null, 2)); + writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, version: '1.0.1' }, null, 2)); }, 100); setTimeout(() => { - writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, name: 'change2' }, null, 2)); + writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, version: '1.0.2' }, null, 2)); }, 150); setTimeout(() => { - writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, name: 'change3' }, null, 2)); + writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, version: '1.0.3' }, null, 2)); }, 200); // Check that only one change event was emitted after debounce await new Promise((resolve) => setTimeout(resolve, 800)); expect(changeCount).to.equal(1); - expect(watcher.getManifest()?.name).to.equal('change3'); + expect((watcher.getManifest() as unknown as Record)?.version).to.equal('1.0.3'); await watcher.stop(); }); }); @@ -418,12 +418,13 @@ describe('ManifestWatcher', () => { eventEmitted = true; }); - writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, name: 'changedApp' }, null, 2)); + writeFileSync(testManifestPath, JSON.stringify({ ...validManifest, version: '2.0.0' }, null, 2)); await new Promise((resolve) => setTimeout(resolve, 300)); expect(eventEmitted).to.be.false; - expect(watcher.getManifest()?.name).to.equal('testApp'); // Still old value + // Watcher stopped before write, so manifest unchanged (validManifest has no version) + expect(watcher.getManifest()).to.deep.equal(validManifest); await watcher.stop(); }); diff --git a/yarn.lock b/yarn.lock index dc543d4..8a816ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1739,10 +1739,10 @@ dependencies: "@salesforce/ts-types" "^2.0.12" -"@salesforce/plugin-command-reference@^3.1.79": - version "3.1.79" - resolved "https://registry.yarnpkg.com/@salesforce/plugin-command-reference/-/plugin-command-reference-3.1.79.tgz#36f1ea069d134ee45489882e28cc9a9bf6db6709" - integrity sha512-t3DH+Ez2ESrY8M8zO6yEodsFq7IYKBseFFRwqFxTf0bIVZcWh4i7UO8oUFQyT9Px+Q3TcnSXw4X9njopxTc2lQ== +"@salesforce/plugin-command-reference@^3.1.77": + version "3.1.78" + resolved "https://registry.npmjs.org/@salesforce/plugin-command-reference/-/plugin-command-reference-3.1.78.tgz" + integrity sha512-8GhAUhYTDD51pAFN6h/wrhD46ZJEs53EMDa//jNIe06nFxs/A2iheJSaVNyf9p+ft1KGTvhg+5WXQBFW5daFsA== dependencies: "@oclif/core" "^4" "@salesforce/core" "^8.23.3" @@ -1758,18 +1758,18 @@ resolved "https://registry.npmjs.org/@salesforce/prettier-config/-/prettier-config-0.0.3.tgz" integrity sha512-hYOhoPTCSYMDYn+U1rlEk16PoBeAJPkrdg4/UtAzupM1mRRJOwEPMG1d7U8DxJFKuXW3DMEYWr2MwAIBDaHmFg== -"@salesforce/sdk-core@^1.22.0": - version "1.22.0" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-core/-/sdk-core-1.22.0.tgz#6488c2a64954ef554253f7d6293239d3e3ba9e61" - integrity sha512-L3GT267pg8iRJFXLUg+DVjn76UgJSwexXhWsAV5WDiLEkXlEwKdGFmpmKYbDx9M9sUN3NckiYw+trWGRjUEHNw== +"@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-data@^1.22.0": - version "1.22.0" - resolved "https://registry.yarnpkg.com/@salesforce/sdk-data/-/sdk-data-1.22.0.tgz#2dbf26f8b29f4bcc56aaf070baa74dbe64d02cd6" - integrity sha512-KH5RcQfyXj0jjvpI7gv54+e7qhiOBZ+XjuBA6UsOuk4bRvRfvqtwxCl1qSTLTU6iEoburudq6ixu1n7A6MOG+g== +"@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== dependencies: "@conduit-client/salesforce-lightning-service-worker" "^3.7.0" - "@salesforce/sdk-core" "^1.22.0" + "@salesforce/sdk-core" "^1.29.1" "@salesforce/sf-plugins-core@^11.3.12": version "11.3.12" @@ -1811,12 +1811,12 @@ integrity sha512-BIJyduJC18Kc8z+arUm5AZ9VkPRyw1KKAm+Tk+9LT99eOzhNilyfKzhZ4t+tG2lIGgnJpmytZfVDZ0e2kFul8g== "@salesforce/webapp-experimental@^1.23.0": - version "1.23.0" - resolved "https://registry.yarnpkg.com/@salesforce/webapp-experimental/-/webapp-experimental-1.23.0.tgz#b95ebfebd3254361732e8edcfbdc56a8b819a948" - integrity sha512-5EKzZ6MFnCzmKdHSSt+28riAIgFQ+5PfPRQg3Gl0mnUA3GzN9XwzEq8hI/r52sDlKPvtb2x+MKAxyYs/182OEg== + version "1.29.1" + resolved "https://registry.yarnpkg.com/@salesforce/webapp-experimental/-/webapp-experimental-1.29.1.tgz#98648cc166b34c1b479f9545e87d10b61dd76adc" + integrity sha512-i5ZzGs7hrjv3Fbrvmm7v8OTVoJRewWvqzXIB5JRW5l2mcz1HxK2DXriBbLHHOmYRcVSrNdN/VS8PW7YwcTuHZw== dependencies: "@salesforce/core" "^8.23.4" - "@salesforce/sdk-data" "^1.22.0" + "@salesforce/sdk-data" "^1.29.1" axios "^1.7.7" micromatch "^4.0.8" path-to-regexp "^8.3.0" From da39c4c99c4cabeb9be453a82f20031e400a5567 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Wed, 18 Feb 2026 12:04:06 +0530 Subject: [PATCH 08/15] Address review comments: URL mismatch tests, multi-meta warning, 60s timeout - Add comprehensive tests for devServerUrl vs actualDevServerUrl mismatch combinations (explicit URL match/mismatch, manifest, skipDevServer cases) - Warn when multiple .webapplication-meta.xml files found in directory; use first match for backward compatibility - Increase dev server startup timeout from 30 to 60 seconds to align with VS Code extension and support slower dev server startups Co-authored-by: Cursor --- src/commands/webapp/dev.ts | 5 +- src/config/webappDiscovery.ts | 24 ++++- test/commands/webapp/dev.test.ts | 152 ++++++++++++++++++++++++++++ test/config/webappDiscovery.test.ts | 16 +++ 4 files changed, 191 insertions(+), 6 deletions(-) diff --git a/src/commands/webapp/dev.ts b/src/commands/webapp/dev.ts index 0959e4f..c3b46d9 100644 --- a/src/commands/webapp/dev.ts +++ b/src/commands/webapp/dev.ts @@ -286,6 +286,7 @@ export default class WebappDev extends SfCommand { this.devServerManager = new DevServerManager({ command: devCommand, cwd: webappDir, + startupTimeout: 60_000, // 60 seconds - aligned with VS Code extension }); // Setup dev server event handlers @@ -314,13 +315,13 @@ export default class WebappDev extends SfCommand { const actualDevServerUrl = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject( - new SfError('❌ Dev server did not start within 30 seconds.', 'DevServerTimeoutError', [ + new SfError('❌ Dev server did not start within 60 seconds.', 'DevServerTimeoutError', [ 'The dev server may be taking longer than expected to start', 'Check if the dev server command is correct in webapplication.json', 'Try running the dev server command manually to see if it starts', ]) ); - }, 30_000); + }, 60_000); this.devServerManager?.on('ready', (url: string) => { clearTimeout(timeout); diff --git a/src/config/webappDiscovery.ts b/src/config/webappDiscovery.ts index e0b8bd4..54562f6 100644 --- a/src/config/webappDiscovery.ts +++ b/src/config/webappDiscovery.ts @@ -16,9 +16,11 @@ import { access, readdir, readFile } from 'node:fs/promises'; import { basename, dirname, join, relative } from 'node:path'; -import { SfError, SfProject } from '@salesforce/core'; +import { Logger, SfError, SfProject } from '@salesforce/core'; import type { WebAppManifest } from './manifest.js'; +const logger = Logger.childFromRoot('WebappDiscovery'); + /** * Default command to run when no webapplication.json manifest is found */ @@ -76,18 +78,32 @@ function isWebapplicationsFolder(folderName: string): boolean { /** * Check if a directory contains a {name}.webapplication-meta.xml file - * Returns the webapp name extracted from the filename, or null if not found + * Returns the webapp 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 { try { const entries = await readdir(dirPath); + const matches: string[] = []; + for (const entry of entries) { const match = WEBAPP_META_XML_PATTERN.exec(entry); if (match) { - return match[1]; // Return the webapp name from the filename + matches.push(match[1]); } } - return null; + + if (matches.length === 0) { + return null; + } + + if (matches.length > 1) { + logger.warn( + `Multiple .webapplication-meta.xml files found in ${dirPath}: ${matches.join(', ')}. Using "${matches[0]}".` + ); + } + + return matches[0]; } catch { return null; } diff --git a/test/commands/webapp/dev.test.ts b/test/commands/webapp/dev.test.ts index 161b7f5..ceb9668 100644 --- a/test/commands/webapp/dev.test.ts +++ b/test/commands/webapp/dev.test.ts @@ -258,4 +258,156 @@ describe('webapp:dev command integration', () => { expect(manifest.dev?.command).to.not.be.empty; }); }); + + /** + * Tests for devServerUrl vs actualDevServerUrl mismatch handling. + * Mirrors the logic in dev.ts (lines 335-342) to enumerate all combinations. + * + * Combinations: + * - explicitUrlProvided: --url flag was passed + * - skipDevServer: --url was reachable, so we never start dev server + * - actualDevServerUrl: URL from DevServerManager 'ready' event (only when we start dev server) + */ + describe('Dev Server URL Mismatch', () => { + /** + * Helper that mirrors the mismatch check logic from dev.ts: + * if (explicitUrlProvided && flags.url && flags.url !== actualDevServerUrl) { this.warn(...) } + */ + function shouldWarnUrlMismatch( + explicitUrlProvided: boolean, + flagsUrl: string | undefined, + actualDevServerUrl: string + ): boolean { + return !!(explicitUrlProvided && flagsUrl && flagsUrl !== actualDevServerUrl); + } + + /** + * Helper that mirrors the final devServerUrl assignment when dev server is started: + * devServerUrl = actualDevServerUrl + */ + function getFinalDevServerUrlWhenStarted(actualDevServerUrl: string): string { + return actualDevServerUrl; + } + + describe('when --url is provided and dev server is started (explicitUrlProvided=true)', () => { + it('should NOT warn when flags.url matches actualDevServerUrl', () => { + const flagsUrl = 'http://localhost:5173'; + const actualDevServerUrl = 'http://localhost:5173'; + + expect(shouldWarnUrlMismatch(true, flagsUrl, actualDevServerUrl)).to.be.false; + expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(flagsUrl); + }); + + it('should warn when flags.url differs from actualDevServerUrl (different port)', () => { + const flagsUrl = 'http://localhost:3000'; + const actualDevServerUrl = 'http://localhost:5173'; + + expect(shouldWarnUrlMismatch(true, flagsUrl, actualDevServerUrl)).to.be.true; + expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl); + }); + + it('should warn when flags.url differs from actualDevServerUrl (localhost vs 127.0.0.1)', () => { + const flagsUrl = 'http://localhost:5173'; + const actualDevServerUrl = 'http://127.0.0.1:5173'; + + expect(shouldWarnUrlMismatch(true, flagsUrl, actualDevServerUrl)).to.be.true; + expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl); + }); + + it('should warn when flags.url differs from actualDevServerUrl (trailing slash)', () => { + const flagsUrl = 'http://localhost:5173'; + const actualDevServerUrl = 'http://localhost:5173/'; + + expect(shouldWarnUrlMismatch(true, flagsUrl, actualDevServerUrl)).to.be.true; + expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl); + }); + + it('should always use actualDevServerUrl as final devServerUrl when dev server is started', () => { + const actualDevServerUrl = 'http://localhost:8080'; + expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl); + }); + }); + + describe('when --url is NOT provided (explicitUrlProvided=false)', () => { + it('should NOT warn - no mismatch check when no explicit URL', () => { + const actualDevServerUrl = 'http://localhost:5173'; + + expect(shouldWarnUrlMismatch(false, undefined, actualDevServerUrl)).to.be.false; + expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl); + }); + + it('should use actualDevServerUrl from dev server (manifest has dev.command)', () => { + const actualDevServerUrl = 'http://localhost:3000'; + expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl); + }); + }); + + describe('when --url is reachable (skipDevServer=true)', () => { + it('should use flags.url as devServerUrl - no actualDevServerUrl, no mismatch possible', () => { + const flagsUrl = 'http://localhost:5173'; + // When skipDevServer, we never get actualDevServerUrl - devServerUrl stays as flags.url + expect(flagsUrl).to.equal('http://localhost:5173'); + }); + }); + + describe('when manifest has dev.url and no --url (skipDevServer=false, no dev server start)', () => { + it('should use manifest.dev.url as devServerUrl - no actualDevServerUrl', () => { + const manifestUrl = 'http://localhost:5173'; + // When manifest.dev.url && !explicitUrlProvided, we use manifest url, don't start dev server + expect(manifestUrl).to.equal('http://localhost:5173'); + }); + }); + + describe('combination matrix: explicitUrlProvided × match/mismatch', () => { + const combinations: Array<{ + explicitUrlProvided: boolean; + flagsUrl: string | undefined; + actualDevServerUrl: string; + expectMismatch: boolean; + description: string; + }> = [ + { + explicitUrlProvided: true, + flagsUrl: 'http://localhost:5173', + actualDevServerUrl: 'http://localhost:5173', + expectMismatch: false, + description: 'explicit URL matches actual', + }, + { + explicitUrlProvided: true, + flagsUrl: 'http://localhost:3000', + actualDevServerUrl: 'http://localhost:5173', + expectMismatch: true, + description: 'explicit URL different port', + }, + { + explicitUrlProvided: true, + flagsUrl: 'http://127.0.0.1:5173', + actualDevServerUrl: 'http://localhost:5173', + expectMismatch: true, + description: 'explicit URL different host', + }, + { + explicitUrlProvided: true, + flagsUrl: 'https://localhost:5173', + actualDevServerUrl: 'http://localhost:5173', + expectMismatch: true, + description: 'explicit URL different protocol', + }, + { + explicitUrlProvided: false, + flagsUrl: undefined, + actualDevServerUrl: 'http://localhost:5173', + expectMismatch: false, + description: 'no explicit URL - no mismatch check', + }, + ]; + + combinations.forEach(({ explicitUrlProvided, flagsUrl, actualDevServerUrl, expectMismatch, description }) => { + it(`should ${expectMismatch ? 'warn' : 'not warn'} when ${description}`, () => { + expect(shouldWarnUrlMismatch(explicitUrlProvided, flagsUrl, actualDevServerUrl)).to.equal(expectMismatch); + }); + }); + }); + }); }); diff --git a/test/config/webappDiscovery.test.ts b/test/config/webappDiscovery.test.ts index a8ea7fe..db01739 100644 --- a/test/config/webappDiscovery.test.ts +++ b/test/config/webappDiscovery.test.ts @@ -285,5 +285,21 @@ describe('webappDiscovery', () => { expect(result.webapp?.name).to.equal('standalone-app'); expect(result.allWebapps).to.have.length(1); }); + + it('should warn and use first match when directory has multiple .webapplication-meta.xml files', async () => { + setupSfdxProject(); + + // Create webapp directory with multiple metadata files (misconfiguration) + const multiMetaPath = join(sfdxWebappsPath, 'multi-meta-app'); + mkdirSync(multiMetaPath, { recursive: true }); + writeFileSync(join(multiMetaPath, 'alpha.webapplication-meta.xml'), ''); + writeFileSync(join(multiMetaPath, 'beta.webapplication-meta.xml'), ''); + + const result = await discoverWebapp(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); + }); }); }); From 1e88b071c17614664b04c572c6fb4720c4de74b7 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Thu, 19 Feb 2026 08:22:18 +0530 Subject: [PATCH 09/15] refactor: discover webapps from all package directories via SfProject Co-authored-by: Cursor --- src/config/webappDiscovery.ts | 84 ++++++++++++++++++++--------- test/config/webappDiscovery.test.ts | 43 ++++++++++++--- 2 files changed, 96 insertions(+), 31 deletions(-) diff --git a/src/config/webappDiscovery.ts b/src/config/webappDiscovery.ts index 54562f6..fa2df5c 100644 --- a/src/config/webappDiscovery.ts +++ b/src/config/webappDiscovery.ts @@ -27,9 +27,10 @@ const logger = Logger.childFromRoot('WebappDiscovery'); export const DEFAULT_DEV_COMMAND = 'npm run dev'; /** - * Standard SFDX path to webapplications folder relative to project root + * Standard metadata path segment for webapplications (relative to package directory). + * Consistent with other metadata types: packagePath/main/default/webapplications */ -const SFDX_WEBAPPLICATIONS_PATH = 'force-app/main/default/webapplications'; +const WEBAPPLICATIONS_RELATIVE_PATH = 'main/default/webapplications'; /** * Pattern to match webapplication metadata XML files @@ -46,7 +47,7 @@ export type DiscoveredWebapp = { relativePath: string; /** Parsed manifest content (null if no webapplication.json found) */ manifest: WebAppManifest | null; - /** Webapp name (from manifest.name or folder name) */ + /** Webapp name (from .webapplication-meta.xml or folder name) */ name: string; /** Whether this webapp has a webapplication.json manifest file */ hasManifest: boolean; @@ -69,11 +70,14 @@ function shouldExcludeDirectory(dirName: string): boolean { return EXCLUDED_DIRECTORIES.has(dirName) || dirName.startsWith('.'); } +/** Folder name for webapplications metadata */ +const WEBAPPLICATIONS_FOLDER = 'webapplications'; + /** * Check if a folder name is the standard webapplications folder */ function isWebapplicationsFolder(folderName: string): boolean { - return folderName === basename(SFDX_WEBAPPLICATIONS_PATH); + return folderName === WEBAPPLICATIONS_FOLDER; } /** @@ -142,17 +146,14 @@ async function tryParseWebappManifest(filePath: string): Promise metaXmlName > folderName + * Resolve webapp 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) - * @param manifest - Parsed manifest (or null) * @returns The resolved webapp name */ -function resolveWebappName(folderName: string, metaXmlName: string | null, manifest: WebAppManifest | null): string { - if (manifest?.name && typeof manifest.name === 'string' && manifest.name.trim()) { - return manifest.name; - } +function resolveWebappName(folderName: string, metaXmlName: string | null): string { return metaXmlName ?? folderName; } @@ -172,6 +173,31 @@ 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. + * + * @param projectRoot - Absolute path to project root (where sfdx-project.json lives) + * @returns Array of absolute paths to webapplications folders that exist + */ +async function getWebapplicationsPathsFromProject(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; + }) + ); + + return existenceChecks.filter((p): p is string => p !== null); + } catch { + return []; + } +} + /** * Check if we're inside a webapplications folder by traversing upward through parent directories. * @@ -275,7 +301,7 @@ async function discoverWebappsInFolder(webappsFolderPath: string, cwd: string): path: webappPath, relativePath: relative(cwd, webappPath) || entry.name, manifest, - name: resolveWebappName(entry.name, metaXmlName, manifest), + name: resolveWebappName(entry.name, metaXmlName), hasManifest: manifest !== null, manifestPath: manifest ? manifestFilePath : null, hasMetaXml: true, @@ -311,7 +337,7 @@ type FindAllWebappsResult = { * * Discovery strategy (in order): * 1. Check if inside a webapplications/ directory (upward search) - * 2. Check for SFDX project and use fixed path: force-app/main/default/webapplications + * 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) * * @param cwd - Directory to search from (defaults to process.cwd()) @@ -330,15 +356,26 @@ async function findAllWebapps(cwd: string = process.cwd()): Promise 0) { + // Discover webapps from all package directories and combine + const webappArrays = await Promise.all( + webappsPaths.map((path) => discoverWebappsInFolder(path, cwd)) + ); + const allWebapps = webappArrays.flat(); + + return { + webapps: allWebapps.sort((a, b) => a.name.localeCompare(b.name)), + currentWebappName: null, + webappsFolderFound: true, + inSfdxProject, + }; } } } @@ -351,7 +388,7 @@ async function findAllWebapps(cwd: string = process.cwd()): Promise/main/default/webapplications/\n' + ' └── my-app/\n' + ' ├── my-app.webapplication-meta.xml (required)\n' + ' └── webapplication.json (optional, for dev config)', @@ -461,7 +497,7 @@ export async function discoverWebapp( throw new SfError( 'No webapp found.\n\n' + 'To use this command, either:\n' + - '1. Run from an SFDX project with webapps in force-app/main/default/webapplications/\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' diff --git a/test/config/webappDiscovery.test.ts b/test/config/webappDiscovery.test.ts index db01739..4d5fc87 100644 --- a/test/config/webappDiscovery.test.ts +++ b/test/config/webappDiscovery.test.ts @@ -44,12 +44,18 @@ describe('webappDiscovery', () => { } /** - * Helper to setup SFDX project structure and mock SfProject.resolveProjectPath + * Helper to setup SFDX project structure and mock SfProject.resolveProjectPath. + * Creates sfdx-project.json with packageDirectories so getUniquePackageDirectories works. */ - function setupSfdxProject(): void { + function setupSfdxProject( + packageDirs: Array<{ path: string; default?: boolean }> = [{ path: 'force-app', default: true }] + ): void { // Create SFDX project structure mkdirSync(sfdxWebappsPath, { recursive: true }); - writeFileSync(join(testDir, 'sfdx-project.json'), '{}'); + writeFileSync( + join(testDir, 'sfdx-project.json'), + JSON.stringify({ packageDirectories: packageDirs }) + ); // Mock SfProject.resolveProjectPath to return testDir SfProject.resolveProjectPath = async () => testDir; } @@ -72,8 +78,9 @@ describe('webappDiscovery', () => { }); afterEach(() => { - // Restore original + // Restore original and clear cached project instances SfProject.resolveProjectPath = originalResolveProjectPath; + SfProject.clearInstances(); // Clean up test directory try { rmSync(testDir, { recursive: true, force: true }); @@ -104,7 +111,10 @@ describe('webappDiscovery', () => { it('should throw error if SFDX project has no webapplications folder', async () => { // Create SFDX project but NOT the webapplications folder - writeFileSync(join(testDir, 'sfdx-project.json'), '{}'); + writeFileSync( + join(testDir, 'sfdx-project.json'), + JSON.stringify({ packageDirectories: [{ path: 'force-app', default: true }] }) + ); SfProject.resolveProjectPath = async () => testDir; try { @@ -185,7 +195,7 @@ describe('webappDiscovery', () => { expect(result.autoSelected).to.be.true; }); - it('should use manifest name when available', async () => { + 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' }); @@ -193,7 +203,8 @@ describe('webappDiscovery', () => { const result = await discoverWebapp(undefined, myAppPath); - expect(result.webapp?.name).to.equal('ManifestName'); + // Name comes from .webapplication-meta.xml (folder-name), not manifest.name + expect(result.webapp?.name).to.equal('folder-name'); expect(result.autoSelected).to.be.true; }); @@ -286,6 +297,24 @@ describe('webappDiscovery', () => { expect(result.allWebapps).to.have.length(1); }); + it('should discover webapps from multiple package directories', async () => { + // Create project with two packages: force-app and packages/einstein + const einsteinWebappsPath = join(testDir, 'packages', 'einstein', 'main', 'default', 'webapplications'); + mkdirSync(einsteinWebappsPath, { recursive: true }); + setupSfdxProject([ + { path: 'force-app', default: true }, + { path: 'packages/einstein', default: false }, + ]); + createWebapp(sfdxWebappsPath, 'force-app-webapp'); + createWebapp(einsteinWebappsPath, 'einstein-webapp'); + + const result = await discoverWebapp(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']); + }); + it('should warn and use first match when directory has multiple .webapplication-meta.xml files', async () => { setupSfdxProject(); From 1cbf5f3257ca9cf10ecc04b94b1b880e858f7711 Mon Sep 17 00:00:00 2001 From: deepu-mungamuri94 <36835750+deepu-mungamuri94@users.noreply.github.com> Date: Fri, 20 Feb 2026 07:46:51 +0530 Subject: [PATCH 10/15] Update SF_WEBAPP_DEV_GUIDE.md Co-authored-by: Brian Buchanan <5377888+bpbuch@users.noreply.github.com> --- SF_WEBAPP_DEV_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_WEBAPP_DEV_GUIDE.md index 348e86f..b26c7b9 100644 --- a/SF_WEBAPP_DEV_GUIDE.md +++ b/SF_WEBAPP_DEV_GUIDE.md @@ -34,7 +34,7 @@ my-sfdx-project/ ├── my-app.webapplication-meta.xml # Required: identifies as webapp ├── package.json ├── src/ - └── webapplication.json # Optional: dev configuration + └── webapplication.json ``` ### 2. Run the command From 50c984410027f5deaa1d61f7952ec83b08d402d9 Mon Sep 17 00:00:00 2001 From: deepu-mungamuri94 <36835750+deepu-mungamuri94@users.noreply.github.com> Date: Fri, 20 Feb 2026 07:47:08 +0530 Subject: [PATCH 11/15] Update SF_WEBAPP_DEV_GUIDE.md Co-authored-by: Brian Buchanan <5377888+bpbuch@users.noreply.github.com> --- SF_WEBAPP_DEV_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_WEBAPP_DEV_GUIDE.md index b26c7b9..1615523 100644 --- a/SF_WEBAPP_DEV_GUIDE.md +++ b/SF_WEBAPP_DEV_GUIDE.md @@ -31,7 +31,7 @@ my-sfdx-project/ ├── sfdx-project.json └── force-app/main/default/webapplications/ └── my-app/ # Your webapp folder - ├── my-app.webapplication-meta.xml # Required: identifies as webapp + ├── my-app.webapplication-meta.xml ├── package.json ├── src/ └── webapplication.json From aba3b9c0a1ef32b75fedca07a9e5072e81f7c697 Mon Sep 17 00:00:00 2001 From: deepu-mungamuri94 <36835750+deepu-mungamuri94@users.noreply.github.com> Date: Fri, 20 Feb 2026 07:47:24 +0530 Subject: [PATCH 12/15] Update SF_WEBAPP_DEV_GUIDE.md Co-authored-by: Brian Buchanan <5377888+bpbuch@users.noreply.github.com> --- SF_WEBAPP_DEV_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_WEBAPP_DEV_GUIDE.md index 1615523..d3f7bc9 100644 --- a/SF_WEBAPP_DEV_GUIDE.md +++ b/SF_WEBAPP_DEV_GUIDE.md @@ -30,7 +30,7 @@ The `sf webapp dev` command enables local development of modern web applications my-sfdx-project/ ├── sfdx-project.json └── force-app/main/default/webapplications/ - └── my-app/ # Your webapp folder + └── my-app/ ├── my-app.webapplication-meta.xml ├── package.json ├── src/ From 7c50c3b4f8efdd513f29b7be1abeb09aa83b8026 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 20 Feb 2026 08:16:25 +0530 Subject: [PATCH 13/15] SF_WEBAPP_DEV_GUIDE: document only dev.command and dev.url for dev command - Remove name, label from --name option (matches folder name) - Simplify picker format, remove label references - Remove Full Configuration example (name, label, version, outputDir) - Add Dev + Routing example with only dev and routing - Update troubleshooting: --name matches folder name only Co-authored-by: Cursor --- SF_WEBAPP_DEV_GUIDE.md | 51 +++++++++++------------------------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/SF_WEBAPP_DEV_GUIDE.md b/SF_WEBAPP_DEV_GUIDE.md index d3f7bc9..78f17e0 100644 --- a/SF_WEBAPP_DEV_GUIDE.md +++ b/SF_WEBAPP_DEV_GUIDE.md @@ -68,7 +68,7 @@ sf webapp dev [OPTIONS] | Option | Short | Description | Default | | -------------- | ----- | ----------------------------------------------- | ------------- | | `--target-org` | `-o` | Salesforce org alias or username | Required | -| `--name` | `-n` | Web application name (from webapplication.json) | Auto-discover | +| `--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 | @@ -177,16 +177,15 @@ When multiple webapps are found, you'll see an interactive prompt: ``` Found 3 webapps in project ? Select the webapp to run: (Use arrow keys) -❯ MyApp - My Application (webapplications/app-one) +❯ app-one (webapplications/app-one) app-two (webapplications/app-two) [no manifest] - CustomName (webapplications/app-three) + app-three (webapplications/app-three) ``` Format: -- **With manifest + label**: `Name - Label (path)` -- **With manifest, no label**: `Name (path)` -- **No manifest**: `name (path) [no manifest]` +- **With manifest**: `folder-name (path)` +- **No manifest**: `folder-name (path) [no manifest]` --- @@ -239,28 +238,14 @@ Browser → Proxy → [Auth Headers Injected] → Salesforce → Response The `webapplication.json` file is **optional**. All fields are also optional - missing fields use defaults. -#### All Fields (All Optional) - -```json -{ - "name": "myApp", - "label": "My Application", - "version": "1.0.0", - "outputDir": "dist", - "dev": { - "command": "npm run dev" - } -} -``` +#### Dev Configuration -| Field | Type | Description | Default | -| ----------- | ------ | ----------------------------------------- | ------------------ | -| `name` | string | Unique identifier (used with --name flag) | Folder name | -| `label` | string | Human-readable display name | None | -| `version` | string | Semantic version (e.g., "1.0.0") | None | -| `outputDir` | string | Build output directory | None (deploy only) | +| Field | Type | Description | Default | +| ------------- | ------ | ------------------------------------- | ------------------------- | +| `dev.command` | string | Command to start dev server | `npm run dev` | +| `dev.url` | string | Dev server URL (when already running) | `http://localhost:5173` | -#### Dev Configuration +All fields are optional. Only specify what you need to override. **Option A: No manifest (uses defaults)** @@ -280,8 +265,6 @@ If no `webapplication.json` exists: } ``` -Only specify what you need to override. - **Option C: Explicit URL (dev server already running)** ```json @@ -337,15 +320,10 @@ Warning: No webapplication.json found for webapp "my-dashboard" Press Ctrl+C to stop ``` -### Example: Full Configuration +### Example: Dev + Routing ```json { - "name": "salesDashboard", - "label": "Sales Dashboard", - "description": "Real-time sales analytics dashboard", - "version": "2.1.0", - "outputDir": "dist", "dev": { "command": "npm run dev" }, @@ -493,10 +471,7 @@ sf webapp dev --name SecondWebApp --target-org myOrg # Error! ### "No webapp found with name X" -The `--name` flag matches either: - -1. The `name` field in `webapplication.json` -2. The folder name (if no manifest or no name in manifest) +The `--name` flag matches the folder name of the webapp. ```bash # This looks for webapp named "myApp" From 16032322acfe868427942572b68ef6502ae53d3d Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 20 Feb 2026 08:24:37 +0530 Subject: [PATCH 14/15] Revert README to main branch Co-authored-by: Cursor --- README.md | 520 +++++++++++++----------------------------------------- 1 file changed, 124 insertions(+), 396 deletions(-) diff --git a/README.md b/README.md index efd03b4..7cbe849 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,68 @@ -# plugin-app-dev +**NOTE: This template for sf plugins is not yet official. Please consult with the Platform CLI team before using this template.** -[![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) +# plugin-webapp + +[![NPM](https://img.shields.io/npm/v/@salesforce/plugin-webapp.svg?label=@salesforce/plugin-webapp)](https://www.npmjs.com/package/@salesforce/plugin-webapp) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-webapp.svg)](https://npmjs.org/package/@salesforce/plugin-webapp) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) + +## Using the template + +This repository provides a template for creating a plugin for the Salesforce CLI. To convert this template to a working plugin: + +1. Please get in touch with the Platform CLI team. We want to help you develop your plugin. +2. Generate your plugin: + + ``` + sf plugins install dev + sf dev generate plugin + + git init -b main + git add . && git commit -m "chore: initial commit" + ``` + +3. Create your plugin's repo in the salesforcecli github org +4. When you're ready, replace the contents of this README with the information you want. + +## Learn about `sf` plugins + +Salesforce CLI plugins are based on the [oclif plugin framework](https://oclif.io/docs/introduction). Read the [plugin developer guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_plugins.meta/sfdx_cli_plugins/cli_plugins_architecture_sf_cli.htm) to learn about Salesforce CLI plugin development. + +This repository contains a lot of additional scripts and tools to help with general Salesforce node development and enforce coding standards. You should familiarize yourself with some of the [node developer packages](#tooling) used by Salesforce. There is also a default circleci config using the [release management orb](https://github.com/forcedotcom/npm-release-management-orb) standards. + +Additionally, there are some additional tests that the Salesforce CLI will enforce if this plugin is ever bundled with the CLI. These test are included by default under the `posttest` script and it is required to keep these tests active in your plugin if you plan to have it bundled. + +### Tooling + +- [@salesforce/core](https://github.com/forcedotcom/sfdx-core) +- [@salesforce/kit](https://github.com/forcedotcom/kit) +- [@salesforce/sf-plugins-core](https://github.com/salesforcecli/sf-plugins-core) +- [@salesforce/ts-types](https://github.com/forcedotcom/ts-types) +- [@salesforce/ts-sinon](https://github.com/forcedotcom/ts-sinon) +- [@salesforce/dev-config](https://github.com/forcedotcom/dev-config) +- [@salesforce/dev-scripts](https://github.com/forcedotcom/dev-scripts) # Salesforce CLI Webapp 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 and deploying web applications that integrate with Salesforce. This plugin provides tools for local development, packaging, and deployment of webapps 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). We always recommend using the latest version of these commands bundled with the CLI, however, you can install a specific version or tag if needed. -## Key Features +## 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 to select webapp at project root -- **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 -- **Manifest Hot Reload**: Edit `webapplication.json` while running - changes apply automatically -- **Health Monitoring**: Displays helpful error pages when dev server is down with auto-refresh -- **Framework Agnostic**: Works with any web framework (React, Vue, Angular, etc.) +- 🔐 **Local Development Proxy** - Run webapps 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 ## Quick Start 1. **Install the plugin:** ```bash - sf plugins install @salesforce/plugin-app-dev + sf plugins install @salesforce/plugin-webapp ``` 2. **Authenticate with Salesforce:** @@ -73,427 +107,121 @@ 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 -``` - ---- - -## 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 # Required: identifies as webapp - ├── package.json - ├── src/ - └── webapplication.json # Optional: dev configuration -``` - -### 2. Run the command - -```bash -sf webapp dev --target-org myOrg --open -``` - -### 3. Start developing - -Browser opens with your app running and Salesforce authentication ready. - -- **With Vite plugin**: Open `http://localhost:5173` (Vite handles proxy) -- **Without Vite plugin**: Open `http://localhost:4545` (standalone proxy) - -> **Note**: `{name}.webapplication-meta.xml` is **required** to identify a valid webapp. The `webapplication.json` is optional - if not present, defaults to `npm run dev` command. - ---- - -## Commands - -### `sf webapp dev` - -Start a local development proxy server for webapp development with Salesforce authentication. - -```bash -sf webapp dev [OPTIONS] -``` - -#### Options - -| Option | Short | Description | Default | -| -------------- | ----- | ----------------------------------------------- | ------------- | -| `--target-org` | `-o` | Salesforce org alias or username | Required | -| `--name` | `-n` | Web application name (from webapplication.json) | 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 webapp -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 proxy port -sf webapp dev --target-org myOrg --port 8080 - -# Connect to existing dev server (proxy-only mode) -sf webapp dev --target-org myOrg --url http://localhost:5173 - -# Debug mode -SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg -``` - ---- - -## Configuration - -### webapplication.json Schema - -The `webapplication.json` file is **optional**. If not present, defaults are used. - -| Field | Type | Description | Default | -| ------------- | ------ | ----------------------------------------- | ------------- | -| `name` | string | Unique identifier (used with --name flag) | Folder name | -| `dev.command` | string | Command to start the dev server | `npm run dev` | -| `dev.url` | string | Dev server URL (skip starting server) | Auto-detect | - -#### Examples - -**No manifest (uses defaults):** -``` -webapplications/my-app/ -├── my-app.webapplication-meta.xml -├── package.json # Has "scripts": { "dev": "vite" } -└── src/ -``` - -**Custom dev command:** -```json -{ - "dev": { - "command": "npm start" - } -} -``` - -**Explicit URL (dev server already running):** -```json -{ - "dev": { - "url": "http://localhost:5173" - } -} -``` - ---- - -## Webapp Discovery - -The command discovers webapps using a deterministic algorithm. Webapps are identified by the presence of a `{name}.webapplication-meta.xml` file (SFDX metadata format). - -### 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 | 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 - -``` -my-sfdx-project/ -├── sfdx-project.json # SFDX project marker -└── force-app/main/default/ - └── webapplications/ # Standard SFDX location - ├── app-one/ - │ ├── app-one.webapplication-meta.xml # Required - │ ├── webapplication.json # Optional - │ ├── package.json - │ └── src/ - └── app-two/ - ├── app-two.webapplication-meta.xml # Required - ├── package.json - └── src/ -``` - -### Interactive Selection - -When at the SFDX project root, you'll see an interactive prompt to select a webapp: - -``` -? Select the webapp to run: (Use arrow keys) -❯ MyApp - app-two - CustomName -``` - ---- - -## Vite Integration (Recommended) - -When using **Vite** as your bundler, the `@salesforce/vite-plugin-webapp-experimental` package provides built-in proxy functionality. - -### Setup - -**1. Install the Vite plugin** - -```bash -npm install -D @salesforce/vite-plugin-webapp-experimental -``` - -**2. Configure vite.config.ts** - -```typescript -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import salesforce from '@salesforce/vite-plugin-webapp-experimental'; - -export default defineConfig({ - plugins: [ - react(), - salesforce() // No configuration needed - ], -}); -``` - -**3. Run the dev command** - -```bash -sf webapp dev --target-org myOrg -``` - -### How It Works - -The CLI automatically detects whether your Vite dev server has the Salesforce plugin by sending a health check request. If the plugin responds with `X-Salesforce-WebApp-Proxy: true`, the CLI skips starting its standalone proxy. - -| Scenario | Proxy Behavior | -|-----------------------------|------------------------------------------------| -| Vite plugin **present** | Uses Vite's built-in proxy (open `:5173`) | -| Vite plugin **not present** | CLI creates standalone proxy (open `:4545`) | - -### Benefits - -| Feature | Vite Plugin | Standalone Proxy | -|-------------------------|--------------------|---------------------------| -| Single port to access | ✅ (5173) | ❌ (proxy 4545, dev 5173) | -| Simpler browser URL | ✅ `localhost:5173`| `localhost:4545` | -| HMR through same port | ✅ Native | ✅ Forwarded | - ---- - -## The `--url` Flag - -The `--url` flag provides control over which dev server URL the proxy uses. - -| Scenario | What Happens | -| ------------------------ | ----------------------------------------------------------------- | -| `--url` is reachable | **Proxy-only mode**: Skips starting dev server, only starts proxy | -| `--url` is NOT reachable | Starts dev server, warns if actual URL differs from `--url` | -| No `--url` provided | Starts dev server automatically, detects URL | - -### Example: Connect to Existing Dev Server - -```bash -# Terminal 1: Start your dev server manually -npm run dev -# Output: Local: http://localhost:5173/ - -# Terminal 2: Connect proxy to your running server -sf webapp dev --url http://localhost:5173 --target-org myOrg +sf plugins install @salesforce/plugin-webapp@x.y.z ``` ---- +## Issues -## Troubleshooting +Please report any issues at https://github.com/forcedotcom/cli/issues -### "No webapp found" or "No valid webapps" +## Contributing -Ensure your webapp has the required `.webapplication-meta.xml` file: +1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md) +2. Create a new issue before starting your project so that we can keep track of + what you are trying to add/fix. That way, we can also offer suggestions or + let you know if there is already an effort in progress. +3. Fork this repository. +4. [Build the plugin locally](#build) +5. Create a _topic_ branch in your fork. Note, this step is recommended but technically not required if contributing using a fork. +6. Edit the code in your fork. +7. Write appropriate tests for your changes. Try to achieve at least 95% code coverage on any new code. No pull request will be accepted without unit tests. +8. Sign CLA (see [CLA](#cla) below). +9. Send us a pull request when you are done. We'll review your code, suggest any needed changes, and merge it in. -``` -webapplications/my-app/ -├── my-app.webapplication-meta.xml # Required! -├── package.json -└── webapplication.json # Optional -``` +### CLA -### "You are inside webapp X but specified --name Y" +External contributors will be required to sign a Contributor's License +Agreement. You can do so by going to https://cla.salesforce.com/sign-cla. -**Solutions:** -- Remove `--name` to use the current webapp -- Navigate to the project root and use `--name` +### Build -### "Dependencies Not Installed" / "command not found" +To build the plugin locally, make sure to have yarn installed and run the following commands: ```bash -cd webapplications/my-app -npm install -``` - -### "Port 4545 already in use" +# Clone the repository +git clone git@github.com:salesforcecli/plugin-webapp -```bash -sf webapp dev --port 8080 --target-org myOrg +# Install the dependencies and compile +yarn && yarn build ``` -### "Authentication Failed" +To use your plugin, run using the local `./bin/dev` or `./bin/dev.cmd` file. ```bash -sf org login web --alias myOrg +# Run using local run file. +./bin/dev hello world ``` -### Debug Mode +There should be no differences when running via the Salesforce CLI or using the local run file. However, it can be useful to link the plugin to do some additional testing or run your commands from anywhere on your machine. ```bash -# Terminal 1: Tail logs -tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev - -# Terminal 2: Run with debug -SF_LOG_LEVEL=debug sf webapp dev --target-org myOrg -``` - ---- - -## Architecture - -### Request Flow - -The command supports two proxy modes: - -**With Vite Plugin:** -``` -Browser → Vite Dev Server (:5173) → Salesforce (with auth) - ↓ - Proxy handles: - • /services/* → Salesforce - • Everything else → Vite HMR -``` - -**Standalone Proxy:** -``` -Browser → Proxy Server (:4545) → Salesforce (with auth) - ↓ - Dev Server (:5173) for static assets +# Link your plugin to the sf cli +sf plugins link . +# To verify +sf plugins ``` -### Request Routing - -| URL Path | Routed To | -|-----------------------------|---------------------| -| `/services/*`, `/lwr/apex/*`| Salesforce (+ auth) | -| Everything else | Dev Server | - ---- - -## 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 -3. Extension executes: `sf webapp dev --target-org --open` -4. Browser opens with the app running - ---- +## Commands -## JSON Output +### `sf webapp dev` -For scripting and CI/CD: +Start a local development proxy server for webapp development with Salesforce authentication. ```bash -sf webapp dev --target-org myOrg --json -``` +USAGE + $ sf webapp dev --name --target-org [options] -```json -{ - "status": 0, - "result": { - "url": "http://localhost:4545", - "devServerUrl": "http://localhost:5173" - } -} -``` +REQUIRED FLAGS + -n, --name= Name of the webapp (must match webapplication.json) + -o, --target-org= Salesforce org to authenticate against ---- +OPTIONAL FLAGS + -u, --url= Dev server URL (overrides webapplication.json) + -p, --port= Proxy server port (default: 4545) + --open Open browser automatically -## Issues +GLOBAL FLAGS + --flags-dir= Import flag values from a directory + --json Format output as json -Please report any issues at https://github.com/forcedotcom/cli/issues +DESCRIPTION + Start a local development proxy server for webapp development. -## Contributing + This command starts a local HTTP proxy server that handles Salesforce + authentication and routes requests between your local dev server and + Salesforce APIs. It automatically spawns and monitors your dev server, + detects the URL, and provides health monitoring. -1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md) -2. Create a new issue before starting your project so that we can keep track of what you are trying to add/fix. -3. Fork this repository. -4. [Build the plugin locally](#build) -5. Create a _topic_ branch in your fork. -6. Edit the code in your fork. -7. Write appropriate tests for your changes. Try to achieve at least 95% code coverage on any new code. -8. Sign CLA (see [CLA](#cla) below). -9. Send us a pull request when you are done. +EXAMPLES + Start proxy with automatic dev server management: -### CLA + $ sf webapp dev --name myapp --target-org myorg --open -External contributors will be required to sign a Contributor's License Agreement. You can do so by going to https://cla.salesforce.com/sign-cla. + Use existing dev server: -### Build - -```bash -# Clone the repository -git clone git@github.com:salesforcecli/plugin-webapp + $ sf webapp dev --name myapp --target-org myorg --url http://localhost:5173 --open -# Install dependencies and compile -yarn && yarn build + Use custom proxy port: -# Run using local dev file -./bin/dev webapp dev --target-org myOrg + $ sf webapp dev --name myapp --target-org myorg --port 8080 --open -# Link to SF CLI for testing -sf plugins link . -sf plugins # Verify +SUPPORTED DEV SERVERS + - Vite + - Create React App (Webpack) + - Next.js + - Any server that outputs http://localhost:PORT -# After code changes, just rebuild -yarn build -``` +FEATURES + - Automatic Salesforce authentication injection + - Intelligent request routing (Salesforce vs dev server) + - WebSocket support for Hot Module Replacement (HMR) + - Beautiful HTML error pages with auto-refresh + - Periodic health monitoring (every 5s) + - Configuration file watching (webapplication.json) + - Graceful shutdown on Ctrl+C -### Project Structure - -``` -plugin-webapp/ -├── src/ -│ ├── commands/webapp/ -│ │ └── 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 -│ ├── server/ -│ │ └── DevServerManager.ts # Dev server process management -│ ├── error/ -│ │ └── DevServerErrorParser.ts # Parse dev server errors -│ └── templates/ -│ ├── ErrorPageRenderer.ts # Browser error page generation -│ └── error-page.html # Error page HTML template -├── messages/ -│ └── webapp.dev.md # CLI messages and help text -└── schemas/ - └── webapp-dev.json # JSON schema for output +SEE ALSO + - Complete Guide: SF_WEBAPP_DEV_GUIDE.md ``` From c505620c323e1f10dc87367230ae1af6220ad307 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 20 Feb 2026 08:27:20 +0530 Subject: [PATCH 15/15] Align README with main branch (plugin-app-dev template) Co-authored-by: Cursor --- README.md | 52 +++++++--------------------------------------------- 1 file changed, 7 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 7cbe849..f1cdd12 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,10 @@ -**NOTE: This template for sf plugins is not yet official. Please consult with the Platform CLI team before using this template.** +# plugin-app-dev -# plugin-webapp +[![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-webapp.svg?label=@salesforce/plugin-webapp)](https://www.npmjs.com/package/@salesforce/plugin-webapp) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-webapp.svg)](https://npmjs.org/package/@salesforce/plugin-webapp) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) +# Salesforce CLI App Dev Plugin -## Using the template - -This repository provides a template for creating a plugin for the Salesforce CLI. To convert this template to a working plugin: - -1. Please get in touch with the Platform CLI team. We want to help you develop your plugin. -2. Generate your plugin: - - ``` - sf plugins install dev - sf dev generate plugin - - git init -b main - git add . && git commit -m "chore: initial commit" - ``` - -3. Create your plugin's repo in the salesforcecli github org -4. When you're ready, replace the contents of this README with the information you want. - -## Learn about `sf` plugins - -Salesforce CLI plugins are based on the [oclif plugin framework](https://oclif.io/docs/introduction). Read the [plugin developer guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_plugins.meta/sfdx_cli_plugins/cli_plugins_architecture_sf_cli.htm) to learn about Salesforce CLI plugin development. - -This repository contains a lot of additional scripts and tools to help with general Salesforce node development and enforce coding standards. You should familiarize yourself with some of the [node developer packages](#tooling) used by Salesforce. There is also a default circleci config using the [release management orb](https://github.com/forcedotcom/npm-release-management-orb) standards. - -Additionally, there are some additional tests that the Salesforce CLI will enforce if this plugin is ever bundled with the CLI. These test are included by default under the `posttest` script and it is required to keep these tests active in your plugin if you plan to have it bundled. - -### Tooling - -- [@salesforce/core](https://github.com/forcedotcom/sfdx-core) -- [@salesforce/kit](https://github.com/forcedotcom/kit) -- [@salesforce/sf-plugins-core](https://github.com/salesforcecli/sf-plugins-core) -- [@salesforce/ts-types](https://github.com/forcedotcom/ts-types) -- [@salesforce/ts-sinon](https://github.com/forcedotcom/ts-sinon) -- [@salesforce/dev-config](https://github.com/forcedotcom/dev-config) -- [@salesforce/dev-scripts](https://github.com/forcedotcom/dev-scripts) - -# Salesforce CLI Webapp Plugin - -A Salesforce CLI plugin for building and deploying 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 web applications that integrate with Salesforce. This plugin provides tools for local development, packaging, and deployment of webapps 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). @@ -62,7 +24,7 @@ We always recommend using the latest version of these commands bundled with the 1. **Install the plugin:** ```bash - sf plugins install @salesforce/plugin-webapp + sf plugins install @salesforce/plugin-app-dev ``` 2. **Authenticate with Salesforce:** @@ -107,7 +69,7 @@ We always recommend using the latest version of these commands bundled with the ## Install ```bash -sf plugins install @salesforce/plugin-webapp@x.y.z +sf plugins install @salesforce/plugin-app-dev@x.y.z ``` ## Issues @@ -139,7 +101,7 @@ To build the plugin locally, make sure to have yarn installed and run the follow ```bash # Clone the repository -git clone git@github.com:salesforcecli/plugin-webapp +git clone git@github.com:salesforcecli/plugin-app-dev # Install the dependencies and compile yarn && yarn build