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 bbbe344..1c55a6f 100644
--- a/src/server/DevServerManager.ts
+++ b/src/server/DevServerManager.ts
@@ -429,11 +429,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}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{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}}
-
-
-
-
-
-
-
-
⚠️ {{ERROR_TITLE}}
-
-
{{ERROR_MESSAGE_TEXT}}
-
-
-
-
-
-
{{AUTO_REFRESH_TEXT}}
-
-
-
{{SUGGESTIONS_TITLE}}
-
-
-
-
-
-
-
-
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"