+ +
+
+

{{ERROR_TITLE}}

+ {{MESSAGE_CONTENT}} +
- .suggestions li { - margin: 8px 0; - line-height: 1.6; - color: #555; - } +
+ {{AUTO_REFRESH_TEXT}} +
- .suggestions code { - background: rgba(255, 255, 255, 0.8); - padding: 2px 6px; - border-radius: 3px; - font-family: 'Courier New', monospace; - color: #c62828; - } +
+

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 webapp.json for the correct dev server URL
  • +
  • This page will auto-refresh when the server is detected
  • +
+
+
- .logs { - background: #263238; - color: #aed581; - padding: 15px; - border-radius: 8px; - font-family: 'Courier New', monospace; - font-size: 0.85em; - line-height: 1.6; - max-height: 200px; - overflow-y: auto; - } + +
+
+

⚠️ Runtime Error: {{ERROR_TYPE}}

+
+

{{ERROR_MESSAGE_TEXT}}

+
+
- .logs .timestamp { - color: #78909c; - } +
+

Stack Trace

+
+ {{FORMATTED_STACK_HTML}} +
+
- .logs .arrow { - color: #64b5f6; - } + - .logs .status-text { - color: #e0e0e0; - } +
+

{{SUGGESTIONS_TITLE}}

+
    + {{SUGGESTIONS_LIST}} +
+
+
- .footer { - margin-top: 30px; - padding-top: 20px; - border-top: 1px solid #e0e0e0; - color: #666; - font-size: 0.9em; - text-align: center; - } + +
+
+

⚠️ {{ERROR_TITLE}}

+
+

{{ERROR_MESSAGE_TEXT}}

+
+
- .refresh-hint { - display: inline-block; - margin-top: 10px; - padding: 8px 16px; - background: #f5f5f5; - border-radius: 6px; - font-size: 0.9em; - } +
+

Error Output

+
+
{{STDERR_OUTPUT}}
+
+
- @media (max-width: 768px) { - .content { - grid-template-columns: 1fr; - } +
+ {{AUTO_REFRESH_TEXT}} +
- .header { - flex-direction: column; - gap: 15px; - } - } - - - - -
-
-
-

Local Dev Proxy

-

Salesforce preview → Proxy → Your dev server

+
+

{{SUGGESTIONS_TITLE}}

+
    + {{SUGGESTIONS_LIST}} +
+
-
{{STATUS}}
-
- -
-

Connect to your dev server

-

- We couldn't find a running dev server at {{DEV_SERVER_URL}}. Start your dev server and this page - will automatically refresh. -

-
-
-

Diagnostics

-
    -
  • - Workspace script detected: - {{WORKSPACE_SCRIPT}} -
  • -
  • - Proxy port reserved: - {{PROXY_URL}} -
  • -
  • - Dev server URL: - {{DEV_SERVER_URL}} -
  • -
  • - Org target: - {{ORG_TARGET}} -
  • -
- -
-
- {{TIMESTAMP}} - waiting for dev server... -
-
- {{TIMESTAMP}} - check {{DEV_SERVER_URL}} unreachable -
-
- {{TIMESTAMP}} - hint ▶ try starting your dev server -
+ +
+

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}}"
-
-

How to Fix

-
-

Start your development server

-
    -
  1. Open a terminal in your project directory
  2. -
  3. Run your dev server command:
    {{WORKSPACE_SCRIPT}}
  4. -
  5. Wait for it to start on {{DEV_SERVER_URL}}
  6. -
  7. This page will auto-refresh when detected
  8. -
+
+
+ [{{TIMESTAMP_FORMATTED}}] + error ▶ {{ERROR_TYPE}} detected
+
+ severity ▶ {{SEVERITY_LABEL}} +
+
-
-

Common Issues

-
    -
  1. Wrong port? Update webapp.json or use --url flag
  2. -
  3. Dev server crashed? Check terminal for errors
  4. -
  5. Port conflict? Dev server may be using a different port
  6. -
+ +
+

⚠️ 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
+ +
+
- + + + + - - +
+ + + \ No newline at end of file diff --git a/src/templates/runtime-error-page.html b/src/templates/runtime-error-page.html deleted file mode 100644 index 70a186b..0000000 --- a/src/templates/runtime-error-page.html +++ /dev/null @@ -1,504 +0,0 @@ - - - - - - Runtime Error - Salesforce Local Dev Proxy - - - - -
Copied!
- -
-
-
-

Local Dev Proxy

-

Salesforce preview → Proxy → Your dev server

-
-
{{SEVERITY_LABEL}}
-
- -
-

Runtime Error Detected

-

- {{ERROR_TYPE}} - {{#ERROR_CODE}} - {{ERROR_CODE}} - {{/ERROR_CODE}} -

-

{{ERROR_MESSAGE}}

-
- -
-
-

Stack Trace

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

Diagnostics

-
    -
  • - Node version: - {{NODE_VERSION}} -
  • -
  • - Platform: - {{PLATFORM}} -
  • -
  • - Process ID: - {{PID}} -
  • -
  • - Heap used: - {{HEAP_USED_MB}} MB -
  • -
  • - Heap total: - {{HEAP_TOTAL_MB}} MB -
  • -
  • - Timestamp: - {{TIMESTAMP_FORMATTED}} -
  • -
- -
-

How to Fix

-
    - {{SUGGESTIONS}} -
-
-
-
- - -
- - diff --git a/test/error/DevServerErrorParser.test.ts b/test/error/DevServerErrorParser.test.ts new file mode 100644 index 0000000..32e9823 --- /dev/null +++ b/test/error/DevServerErrorParser.test.ts @@ -0,0 +1,221 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { DevServerErrorParser } from '../../src/error/DevServerErrorParser.js'; + +describe('DevServerErrorParser', () => { + describe('parseError', () => { + it('should parse port conflict errors', () => { + const stderr = ` +Error: listen EADDRINUSE: address already in use 127.0.0.1:5173 + at Server.setupListenHandle [as _listen2] (node:net:1740:16) + at listenInCluster (node:net:1788:12) + `; + + const result = DevServerErrorParser.parseError(stderr, 1, null); + + expect(result.type).to.equal('port-conflict'); + expect(result.title).to.equal('Port Already in Use'); + expect(result.message).to.include('Port 5173'); + expect(result.message).to.include('already in use'); + expect(result.suggestions).to.have.length.greaterThan(0); + expect(result.suggestions[0]).to.include('kill'); + expect(result.stderrLines).to.be.an('array'); + }); + + it('should parse missing module errors', () => { + const stderr = ` +Error: Cannot find module 'vite' +Require stack: +- /Users/test/project/vite.config.js + at Module._resolveFilename (node:internal/modules/cjs/loader:1145:15) + `; + + const result = DevServerErrorParser.parseError(stderr, 1, null); + + expect(result.type).to.equal('missing-module'); + expect(result.title).to.equal('Missing Dependencies'); + expect(result.message).to.include('vite'); + expect(result.suggestions).to.have.length.greaterThan(0); + expect(result.suggestions.some((s) => s.includes('npm install'))).to.be.true; + }); + + it('should parse syntax errors', () => { + const stderr = ` +/Users/test/project/vite.config.js:12:5 +SyntaxError: Unexpected token '}' + at Module._compile (node:internal/modules/cjs/loader:1358:14) + `; + + const result = DevServerErrorParser.parseError(stderr, 1, null); + + expect(result.type).to.equal('syntax-error'); + expect(result.title).to.equal('Configuration Syntax Error'); + expect(result.message).to.include('syntax error'); + expect(result.suggestions).to.have.length.greaterThan(0); + expect(result.suggestions.some((s) => s.toLowerCase().includes('comma'))).to.be.true; + }); + + it('should parse permission errors', () => { + const stderr = ` +Error: EACCES: permission denied, open '/etc/config.json' + at Object.openSync (node:fs:590:3) + `; + + const result = DevServerErrorParser.parseError(stderr, 1, null); + + expect(result.type).to.equal('permission-error'); + expect(result.title).to.equal('Permission Error'); + expect(result.suggestions).to.have.length.greaterThan(0); + expect(result.suggestions.some((s) => s.includes('permissions'))).to.be.true; + }); + + it('should parse file not found errors', () => { + const stderr = ` +Error: ENOENT: no such file or directory, open 'package.json' + at Object.openSync (node:fs:590:3) + `; + + const result = DevServerErrorParser.parseError(stderr, 1, null); + + expect(result.type).to.equal('file-not-found'); + expect(result.title).to.equal('File Not Found'); + expect(result.message).to.include('package.json'); + expect(result.suggestions).to.have.length.greaterThan(0); + }); + + it('should handle unknown errors with fallback', () => { + const stderr = ` +Some random error that doesn't match any pattern +This is completely custom + `; + + const result = DevServerErrorParser.parseError(stderr, 1, null); + + expect(result.type).to.equal('unknown'); + expect(result.title).to.equal('Dev Server Failed to Start'); + expect(result.suggestions).to.have.length.greaterThan(0); + expect(result.stderrLines).to.be.an('array'); + }); + + it('should include exit code and signal in result', () => { + const stderr = 'Error occurred'; + + const result = DevServerErrorParser.parseError(stderr, 127, 'SIGTERM'); + + expect(result.exitCode).to.equal(127); + expect(result.signal).to.equal('SIGTERM'); + }); + + it('should filter out noise from stderr lines', () => { + const stderr = ` +npm WARN deprecated package@1.0.0 +Error: EADDRINUSE: port already in use +npm ERR! code 1 + at Server.listen + `; + + const result = DevServerErrorParser.parseError(stderr, 1, null); + + expect(result.type).to.equal('port-conflict'); + // Should not include npm WARN/ERR lines + expect(result.stderrLines.join('\n')).to.not.include('npm WARN'); + expect(result.stderrLines.join('\n')).to.not.include('npm ERR'); + }); + + it('should limit stderr lines to maximum', () => { + const longStderr = Array.from({ length: 100 }, (_, i) => `Line ${i}`).join('\n'); + + const result = DevServerErrorParser.parseError(longStderr, 1, null); + + expect(result.stderrLines.length).to.be.lessThan(20); + }); + + it('should extract port number from various formats', () => { + const stderrFormats = [ + 'Error: Port 5173 is already in use', + 'EADDRINUSE: address already in use :5173', + 'listen EADDRINUSE 127.0.0.1:5173', + ]; + + for (const stderr of stderrFormats) { + const result = DevServerErrorParser.parseError(stderr, 1, null); + expect(result.type).to.equal('port-conflict'); + expect(result.message).to.include('5173'); + expect(result.suggestions[0]).to.include('5173'); + } + }); + + it('should extract module name from error', () => { + const stderrFormats = [ + "Error: Cannot find module 'vite'", + "Module not found: 'react'", + 'MODULE_NOT_FOUND: @vitejs/plugin-react', + ]; + + for (const stderr of stderrFormats) { + const result = DevServerErrorParser.parseError(stderr, 1, null); + expect(result.type).to.equal('missing-module'); + expect(result.title).to.equal('Missing Dependencies'); + } + }); + }); + + describe('shouldRetry', () => { + it('should not retry permanent errors', () => { + const permanentErrors = [ + { type: 'syntax-error' as const, title: '', message: '', stderrLines: [], suggestions: [] }, + { type: 'missing-module' as const, title: '', message: '', stderrLines: [], suggestions: [] }, + { type: 'file-not-found' as const, title: '', message: '', stderrLines: [], suggestions: [] }, + { type: 'permission-error' as const, title: '', message: '', stderrLines: [], suggestions: [] }, + ]; + + for (const error of permanentErrors) { + expect(DevServerErrorParser.shouldRetry(error)).to.be.false; + } + }); + + it('should retry transient errors', () => { + const transientErrors = [ + { type: 'port-conflict' as const, title: '', message: '', stderrLines: [], suggestions: [] }, + { type: 'unknown' as const, title: '', message: '', stderrLines: [], suggestions: [] }, + ]; + + for (const error of transientErrors) { + expect(DevServerErrorParser.shouldRetry(error)).to.be.true; + } + }); + }); + + describe('getSummary', () => { + it('should generate concise summary', () => { + const error = { + type: 'port-conflict' as const, + title: 'Port Already in Use', + message: 'Port 5173 is already in use', + stderrLines: [], + suggestions: [], + }; + + const summary = DevServerErrorParser.getSummary(error); + + expect(summary).to.equal('Port Already in Use: Port 5173 is already in use'); + expect(summary).to.not.include('suggestions'); + expect(summary).to.not.include('stderr'); + }); + }); +}); diff --git a/test/templates/ErrorPageRenderer.test.ts b/test/templates/ErrorPageRenderer.test.ts index e53c6d7..86dfbca 100644 --- a/test/templates/ErrorPageRenderer.test.ts +++ b/test/templates/ErrorPageRenderer.test.ts @@ -220,38 +220,6 @@ describe('ErrorPageRenderer', () => { }); }); - describe('Fallback Pages', () => { - it('should render fallback dev server error page', () => { - const html = ErrorPageRenderer.renderFallback('http://localhost:5173'); - - expect(html).to.include('Dev Server Unavailable'); - expect(html).to.include('http://localhost:5173'); - expect(html).to.include('Start your dev server'); - expect(html).to.include('auto-refresh'); - }); - - it('should render fallback runtime error page', () => { - const html = ErrorPageRenderer.renderRuntimeErrorFallback( - 'TypeError', - 'Cannot read property of undefined', - 'stack trace here' - ); - - expect(html).to.include('Runtime Error'); - expect(html).to.include('TypeError'); - expect(html).to.include('Cannot read property of undefined'); - expect(html).to.include('stack trace here'); - }); - - it('should render fallback pages without errors', () => { - const html = ErrorPageRenderer.renderRuntimeErrorFallback('TypeError', 'Test error message', 'stack trace here'); - - expect(html).to.be.a('string'); - expect(html).to.include('TypeError'); - expect(html).to.include('Test error message'); - }); - }); - describe('Template Loading', () => { it('should load templates successfully', () => { expect(() => new ErrorPageRenderer()).to.not.throw(); From ccf50a77c5f63b9763e7c87178df339cf7bf9ea0 Mon Sep 17 00:00:00 2001 From: Nicolas Kruk Date: Wed, 3 Dec 2025 01:06:52 -0500 Subject: [PATCH 12/36] fix: use volta --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b1ce30..5cec3eb 100644 --- a/package.json +++ b/package.json @@ -202,5 +202,9 @@ } }, "exports": "./lib/index.js", - "type": "module" + "type": "module", + "volta": { + "node": "20.19.5", + "yarn": "1.22.22" + } } From d326b3100cd0be712ce7c6d254b5e408c5c683fb Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Wed, 3 Dec 2025 22:58:02 +0530 Subject: [PATCH 13/36] fix(dev): resolve Ctrl+C not exiting when using explicit URL mode - Use prependOnceListener for SIGINT/SIGTERM before sfCommand handlers - Ensures graceful shutdown when no dev server is spawned - Works with explicit URL in webapp.json or --url flag - Revert .gitignore changes (documentation files removed) --- .gitignore | 4 +--- src/commands/webapp/dev.ts | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 7471a46..2fbeb2c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,10 +22,8 @@ lib coverage test_session* -# generated docs (but allow markdown documentation) +# generated docs docs -!docs/*.md -!docs/README.md # ignore sfdx-trust files *.tgz diff --git a/src/commands/webapp/dev.ts b/src/commands/webapp/dev.ts index 8dae276..4358917 100644 --- a/src/commands/webapp/dev.ts +++ b/src/commands/webapp/dev.ts @@ -298,15 +298,28 @@ export default class WebappDev extends SfCommand { // Keep the command running until interrupted or dev server exits await new Promise((resolve) => { // Exit if dev server exits with SIGINT (user pressed Ctrl+C) - this.devServerManager?.on('exit', (code: number | null, signal: string | null) => { - if (signal === 'SIGINT') { - this.logger?.debug('Dev server received SIGINT, exiting command'); - resolve(); - } + if (this.devServerManager) { + this.devServerManager.on('exit', (code: number | null, signal: string | null) => { + if (signal === 'SIGINT') { + this.logger?.debug('Dev server received SIGINT, exiting command'); + resolve(); + } + }); + } + + // CRITICAL: Use prependOnceListener to add our handlers BEFORE sfCommand's handlers + // sfCommand adds process.on('SIGINT', () => this.exit(130)) which throws ExitError + // By using prependOnceListener, our resolve() runs FIRST, allowing clean shutdown + // This is especially important when there's no dev server (explicit URL mode) + process.prependOnceListener('SIGINT', () => { + this.logger?.debug('Received SIGINT signal, initiating graceful shutdown'); + resolve(); }); - // The command will also exit on process SIGINT/SIGTERM - // which triggers the finally() cleanup + process.prependOnceListener('SIGTERM', () => { + this.logger?.debug('Received SIGTERM signal, initiating graceful shutdown'); + resolve(); + }); }); // Return result (never reached, but required for type safety) From 399c86439b7434f50022e70e4bda4a0475ce3b11 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Thu, 4 Dec 2025 03:44:52 +0530 Subject: [PATCH 14/36] refactor: cleanup logging, remove debug flag, and remove GlobalErrorCapture - Replace custom Logger with @salesforce/core Logger and this.log() - Remove --debug flag (use SF_LOG_LEVEL=debug instead) - Remove GlobalErrorCapture and related runtime error handling - Remove StackTraceFormatter and error types - Add copy-resources build step for schemas and templates - Add open npm package dependency - Standardize logger variable naming across files - Clean up unused messages from webapp.dev.md - Update tests to reflect removed functionality --- command-snapshot.json | 4 +- messages/webapp.dev.md | 221 +----------- package.json | 30 +- src/auth/AuthManager.ts | 15 +- src/commands/webapp/dev.ts | 142 ++------ src/config/types.ts | 4 +- src/error/ErrorHandler.ts | 142 +------- src/error/GlobalErrorCapture.ts | 426 ----------------------- src/error/StackTraceFormatter.ts | 270 -------------- src/error/types.ts | 260 -------------- src/proxy/ProxyServer.ts | 286 ++++----------- src/proxy/RequestRouter.ts | 25 -- src/server/DevServerManager.ts | 76 ++-- src/templates/ErrorPageRenderer.ts | 185 +++------- src/utils/Logger.ts | 73 ---- test/auth/AuthManager.test.ts | 46 ++- test/commands/webapp/dev.test.ts | 22 -- test/error/ErrorHandler.test.ts | 142 -------- test/error/GlobalErrorCapture.test.ts | 341 ------------------ test/error/StackTraceFormatter.test.ts | 239 ------------- test/proxy/ProxyServer.test.ts | 15 - test/proxy/RequestRouter.test.ts | 15 - test/server/DevServerManager.test.ts | 75 ++-- test/templates/ErrorPageRenderer.test.ts | 160 --------- test/utils/Logger.test.ts | 299 ---------------- yarn.lock | 66 ++++ 26 files changed, 354 insertions(+), 3225 deletions(-) delete mode 100644 src/error/GlobalErrorCapture.ts delete mode 100644 src/error/StackTraceFormatter.ts delete mode 100644 src/error/types.ts delete mode 100644 src/utils/Logger.ts delete mode 100644 test/error/GlobalErrorCapture.test.ts delete mode 100644 test/error/StackTraceFormatter.test.ts delete mode 100644 test/utils/Logger.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index bf64f7a..4f5dc98 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -3,8 +3,8 @@ "alias": [], "command": "webapp:dev", "flagAliases": [], - "flagChars": ["b", "d", "n", "o", "p", "u"], - "flags": ["debug", "flags-dir", "json", "name", "open", "port", "target-org", "url"], + "flagChars": ["b", "n", "o", "p", "u"], + "flags": ["flags-dir", "json", "name", "open", "port", "target-org", "url"], "plugin": "@salesforce/plugin-webapp" }, { diff --git a/messages/webapp.dev.md b/messages/webapp.dev.md index cdaaf2d..35298c3 100644 --- a/messages/webapp.dev.md +++ b/messages/webapp.dev.md @@ -16,10 +16,6 @@ Identifies the Web Application The unique name of the web application as defined in webapp.json. This is used to load the appropriate configuration and settings. -# flags.target.summary - -Selects which Web Application target to use for the preview (e.g., Lightning App, Site) - # flags.url.summary Dev server origin to forward UI/HMR/static requests @@ -36,30 +32,10 @@ Local proxy port The port on which the proxy server will listen. Your browser should connect to this port, not directly to the dev server. The proxy will forward authenticated requests to Salesforce and other requests to your dev server. -# flags.target-org.summary - -Org to use for auth - -# flags.target-org.description - -The Salesforce org alias or username to use for authentication. The proxy will use this org's credentials to authenticate API requests to Salesforce. - -# flags.debug.summary - -Enable verbose proxy logging - -# flags.debug.description - -When enabled, the proxy will log detailed information about each request including headers, routing decisions, and response status. Useful for debugging authentication or routing issues. Note: Access tokens are never logged even in debug mode. - # flags.open.summary Auto-open proxy URL in default browser -# flags.open.description - -When enabled, automatically opens the proxy URL in your default browser after the proxy server starts. - # examples - Start the development server with explicit dev server URL: @@ -74,49 +50,21 @@ When enabled, automatically opens the proxy URL in your default browser after th <%= config.bin %> <%= command.id %> --name myWebApp --target-org myorg --port 4546 --open -- Start with debug logging: - - <%= config.bin %> <%= command.id %> --name myWebApp --target-org myorg --debug - -# info.loading-manifest +- Start with debug logging (using SF_LOG_LEVEL environment variable): -Loading webapp.json manifest... - -# info.manifest-loaded - -Manifest loaded: %s + SF_LOG_LEVEL=debug <%= config.bin %> <%= command.id %> --name myWebApp --target-org myorg # info.manifest-changed Manifest %s detected -# info.using-explicit-url - -Using explicit dev server URL: %s - -# info.using-manifest-url - -Using dev server URL from manifest: %s - -# info.initializing-auth - -Initializing authentication for org: %s - -# info.starting-proxy - -Starting proxy server on port %s... - -# info.starting - -Starting development server for web app: %s - -# info.using-target-org +# info.manifest-reloaded -Using target org: %s +✓ Manifest reloaded successfully -# info.proxy-running +# info.dev-url-changed -✓ Proxy server running on %s +Dev server URL updated to: %s # info.dev-server-url @@ -126,10 +74,6 @@ Dev server URL: %s Proxy URL: %s (open this in your browser) -# info.opening-browser - -Opening browser... - # info.ready-for-development ✓ Ready for development! @@ -138,21 +82,17 @@ Opening browser... Press Ctrl+C to stop the server -# info.shutting-down - -Shutting down (%s)... - -# info.dev-server-ready +# info.dev-server-healthy -✓ Dev server ready at: %s +✓ Dev server is responding at: %s -# info.dev-server-exit +# info.dev-server-detected -Dev server stopped +✅ Dev server detected at %s -# info.dev-server-healthy +# info.start-dev-server-hint -✓ Dev server is responding at: %s +Start your dev server to continue development # warning.dev-server-not-responding @@ -162,148 +102,23 @@ Dev server stopped ⚠ Dev server is not responding at: %s +# warning.dev-server-unreachable-status + +⚠️ Dev server unreachable at %s + # warning.dev-server-start-hint The proxy server is running, but the dev server may not be started yet. Make sure to start your dev server (e.g., 'npm run dev') before opening the browser. -# info.starting-dev-server - -Starting dev server with command: %s - -# info.dev-server-started - -Dev server started at: %s - -# info.watching-manifest - -Watching webapp.json for changes... - -# info.manifest-reloaded - -✓ Manifest reloaded successfully - -# info.dev-url-changed +# warning.dev-command-changed -Dev server URL updated to: %s +dev.command changed to "%s" - restart the command to apply this change. # error.manifest-watch-failed Failed to watch manifest: %s -# error.manifest-not-found - -webapp.json not found in the current directory. Run 'sf webapp generate' to create a new web app. - -# error.manifest-invalid - -Invalid webapp.json: %s - -# error.manifest-validation-failed - -Manifest validation failed: -%s - -# error.org-not-found - -Org '%s' not found. Check available orgs with 'sf org list'. - -# error.org-auth-failed - -Failed to authenticate with org '%s'. Run 'sf org login web --alias %s' to re-authenticate. - -# error.token-expired - -Your org authentication has expired. Run 'sf org login web --alias %s' to re-authenticate. - -# error.token-refresh-failed - -Failed to refresh access token. Run 'sf org login web --alias %s' to re-authenticate. - # error.dev-server-failed Dev server failed to start: %s - -# error.dev-server-command-required - -No dev server URL provided and webapp.json does not contain dev.command or dev.url. Please provide --url flag or configure dev settings in webapp.json. - -# error.dev-server-timeout - -Dev server did not start within 30 seconds. Check the command in webapp.json and ensure your dev server outputs its URL to stdout. - -# error.port-in-use - -Port %s is already in use. Try a different port with --port flag. - -# error.proxy-start-failed - -Failed to start proxy server: %s - -# error.network-error - -Network error: %s. Check your internet connection. - -# error.request-failed - -Request to %s failed: %s - -# error.invalid-url - -Invalid URL: %s - -# error.connection-refused - -Connection refused to %s. Ensure the dev server is running. - -# warning.no-manifest-dev-config - -webapp.json does not contain dev configuration. Using provided --url flag. - -# warning.manifest-dev-command-override - -webapp.json contains dev.command but --url flag provided. Using --url flag. - -# warning.dev-command-changed - -dev.command changed to "%s" - restart the command to apply this change. - -# warning.dev-server-stderr - -Dev server error: %s - -# debug.request-received - -[REQUEST] %s %s - -# debug.routing-decision - -[ROUTING] %s -> %s (auth: %s) - -# debug.auth-headers-injected - -[AUTH] Authorization headers injected - -# debug.proxying-request - -[PROXY] Forwarding to %s - -# debug.response-received - -[RESPONSE] %s %s - -# debug.websocket-upgrade - -[WEBSOCKET] Upgrading connection for %s - -# debug.manifest-change-detected - -[MANIFEST] Change detected, reloading... - -# debug.token-refresh-attempt - -[AUTH] Token expired, attempting refresh... - -# debug.token-refresh-success - -[AUTH] Token refresh successful diff --git a/package.json b/package.json index 5cec3eb..5666b3b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "@salesforce/sf-plugins-core": "^12", "ajv": "^8.12.0", "chokidar": "^3.6.0", - "http-proxy": "^1.18.1" + "http-proxy": "^1.18.1", + "open": "^10.1.0" }, "devDependencies": { "@oclif/plugin-command-snapshot": "^5.3.8", @@ -68,6 +69,7 @@ "clean": "sf-clean", "clean-all": "sf-clean all", "compile": "wireit", + "copy-resources": "wireit", "docs": "sf-docs", "fix-license": "eslint src test --fix --rule \"header/header: [2]\"", "format": "wireit", @@ -88,24 +90,36 @@ "build": { "dependencies": [ "compile", + "copy-resources", "lint" ] }, "compile": { - "command": "tsc -p . --pretty --incremental && mkdir -p lib/schemas && cp src/schemas/*.json lib/schemas/ && mkdir -p lib/templates && cp src/templates/*.html lib/templates/", + "command": "tsc -p . --pretty --incremental", "files": [ "src/**/*.ts", - "src/**/*.json", - "src/**/*.html", "**/tsconfig.json", "messages/**" ], "output": [ - "lib/**", + "lib/**/*.js", + "lib/**/*.d.ts", + "lib/**/*.js.map", "*.tsbuildinfo" ], "clean": "if-file-deleted" }, + "copy-resources": { + "command": "mkdir -p lib/schemas lib/templates && cp src/schemas/*.json lib/schemas/ && cp src/templates/*.html lib/templates/", + "files": [ + "src/schemas/**", + "src/templates/*.html" + ], + "output": [ + "lib/schemas/**", + "lib/templates/*.html" + ] + }, "format": { "command": "prettier --write \"+(src|test|schemas)/**/*.+(ts|js|json)|command-snapshot.json\"", "files": [ @@ -202,9 +216,5 @@ } }, "exports": "./lib/index.js", - "type": "module", - "volta": { - "node": "20.19.5", - "yarn": "1.22.22" - } + "type": "module" } diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 75204c2..8ed31f9 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { Connection, Org, SfError } from '@salesforce/core'; +import { Connection, Logger, Org, SfError } from '@salesforce/core'; import { AuthHeaders } from '../config/types.js'; -import { Logger } from '../utils/Logger.js'; /** * Manages authentication for Salesforce API requests @@ -25,18 +24,20 @@ import { Logger } from '../utils/Logger.js'; export class AuthManager { private org: Org | null = null; private connection: Connection | null = null; - private logger: Logger; + private logger: Logger | null = null; private targetOrg: string; - public constructor(targetOrg: string, logger: Logger) { + public constructor(targetOrg: string) { this.targetOrg = targetOrg; - this.logger = logger; } /** * Initialize the auth manager by loading the org */ public async initialize(): Promise { + // Initialize logger + this.logger = await Logger.child('AuthManager'); + try { this.org = await Org.create({ aliasOrUsername: this.targetOrg }); this.connection = this.org.getConnection(); @@ -108,7 +109,7 @@ export class AuthManager { } try { - this.logger.debug('Token expired, attempting refresh...'); + this.logger?.debug('Token expired, attempting refresh...'); // Recreate the org connection to force a token refresh this.org = await Org.create({ aliasOrUsername: this.targetOrg }); @@ -117,7 +118,7 @@ export class AuthManager { // Force a token refresh by making a lightweight request await this.connection.request('/services/data'); - this.logger.debug('Token refresh successful'); + this.logger?.debug('Token refresh successful'); } catch (error) { if (error instanceof Error) { throw new SfError( diff --git a/src/commands/webapp/dev.ts b/src/commands/webapp/dev.ts index 4358917..4b27d89 100644 --- a/src/commands/webapp/dev.ts +++ b/src/commands/webapp/dev.ts @@ -14,17 +14,15 @@ * limitations under the License. */ -import { spawn } from 'node:child_process'; +import open from 'open'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; -import { Messages, SfError } from '@salesforce/core'; +import { Logger, Messages, SfError } from '@salesforce/core'; import type { WebAppDevResult, WebAppManifest, DevServerError } from '../../config/types.js'; import { ManifestWatcher } from '../../config/ManifestWatcher.js'; import { DevServerManager } from '../../server/DevServerManager.js'; import { AuthManager } from '../../auth/AuthManager.js'; import { ProxyServer } from '../../proxy/ProxyServer.js'; -import { Logger } from '../../utils/Logger.js'; import { ErrorHandler } from '../../error/ErrorHandler.js'; -import { GlobalErrorCapture } from '../../error/GlobalErrorCapture.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-webapp', 'webapp.dev'); @@ -53,17 +51,7 @@ export default class WebappDev extends SfCommand { char: 'p', default: 4545, }), - 'target-org': Flags.requiredOrg({ - summary: messages.getMessage('flags.target-org.summary'), - description: messages.getMessage('flags.target-org.description'), - char: 'o', - required: true, - }), - debug: Flags.boolean({ - summary: messages.getMessage('flags.debug.summary'), - char: 'd', - default: false, - }), + 'target-org': Flags.requiredOrg(), open: Flags.boolean({ summary: messages.getMessage('flags.open.summary'), char: 'b', @@ -75,37 +63,21 @@ export default class WebappDev extends SfCommand { private devServerManager: DevServerManager | null = null; private proxyServer: ProxyServer | null = null; private logger: Logger | null = null; - private errorCapture: GlobalErrorCapture | null = null; /** * Open the proxy URL in the default browser */ - private static openBrowser(url: string): void { - const platform = process.platform; - let command: string; - - if (platform === 'darwin') { - command = 'open'; - } else if (platform === 'win32') { - command = 'start'; - } else { - command = 'xdg-open'; - } - - const child = spawn(command, [url], { - detached: true, - stdio: 'ignore', - }); - - child.unref(); + private static async openBrowser(url: string): Promise { + await open(url); } // eslint-disable-next-line complexity public async run(): Promise { const { flags } = await this.parse(WebappDev); - // Initialize logger - this.logger = new Logger(flags.debug); + // Initialize logger from @salesforce/core for debug logging + // Logger respects SF_LOG_LEVEL environment variable + this.logger = await Logger.child('WebappDev'); // Declare variables outside try block for catch block access let manifest: WebAppManifest | null = null; @@ -114,7 +86,7 @@ export default class WebappDev extends SfCommand { try { // Step 1: Load and validate manifest - this.logger.debug(messages.getMessage('info.loading-manifest')); + this.logger.debug('Loading webapp.json manifest...'); const manifestPath = 'webapp.json'; this.manifestWatcher = new ManifestWatcher({ manifestPath, watch: true }); @@ -125,26 +97,26 @@ export default class WebappDev extends SfCommand { throw ErrorHandler.createManifestNotFoundError(); } - this.logger.debug(messages.getMessage('info.manifest-loaded', [manifest.name])); + this.logger.debug(`Manifest loaded: ${manifest.name}`); // Setup manifest change handler this.manifestWatcher.on('change', (event) => { - this.logger?.info(messages.getMessage('info.manifest-changed', [event.type])); + this.log(messages.getMessage('info.manifest-changed', [event.type])); if (event.type === 'changed' && event.manifest) { - this.logger?.info(messages.getMessage('info.manifest-reloaded')); + this.log(messages.getMessage('info.manifest-reloaded')); // Check for dev.url changes (can be updated dynamically) const oldDevUrl = manifest?.dev?.url; const newDevUrl = event.manifest.dev?.url; if (newDevUrl && oldDevUrl !== newDevUrl) { - this.logger?.info(messages.getMessage('info.dev-url-changed', [newDevUrl])); + this.log(messages.getMessage('info.dev-url-changed', [newDevUrl])); this.proxyServer?.updateDevServerUrl(newDevUrl); } // Check for dev.command changes (cannot be changed while running) if (event.manifest.dev?.command && event.manifest.dev.command !== manifest?.dev?.command) { - this.logger?.warn(messages.getMessage('warning.dev-command-changed', [event.manifest.dev.command])); + this.warn(messages.getMessage('warning.dev-command-changed', [event.manifest.dev.command])); } // Update manifest reference to reflect all changes @@ -153,7 +125,7 @@ export default class WebappDev extends SfCommand { }); this.manifestWatcher.on('error', (error: SfError) => { - this.logger?.error(messages.getMessage('error.manifest-watch-failed', [error.message])); + this.warn(messages.getMessage('error.manifest-watch-failed', [error.message])); }); // Step 2: Determine dev server URL @@ -161,22 +133,21 @@ export default class WebappDev extends SfCommand { // Priority: --url flag > manifest dev.url > spawn dev.command if (flags.url) { devServerUrl = flags.url; - this.logger.debug(messages.getMessage('info.using-explicit-url', [devServerUrl])); + this.logger.debug(`Using explicit dev server URL: ${devServerUrl}`); } else if (manifest.dev?.url) { devServerUrl = manifest.dev.url; - this.logger.debug(messages.getMessage('info.using-manifest-url', [devServerUrl])); + this.logger.debug(`Using dev server URL from manifest: ${devServerUrl}`); } else if (manifest.dev?.command) { // Start dev server - this.logger.debug(messages.getMessage('info.starting-dev-server', [manifest.dev.command])); + this.logger.debug(`Starting dev server with command: ${manifest.dev.command}`); this.devServerManager = new DevServerManager({ command: manifest.dev.command, cwd: process.cwd(), - debug: flags.debug, }); // Setup dev server event handlers this.devServerManager.on('ready', (url: string) => { - this.logger?.debug(messages.getMessage('info.dev-server-ready', [url])); + this.logger?.debug(`Dev server ready at: ${url}`); // Clear any dev server error when server starts successfully this.proxyServer?.clearActiveDevServerError(); }); @@ -185,19 +156,19 @@ export default class WebappDev extends SfCommand { // Check if this is a parsed dev server error (has DevServerError-specific fields) if ('stderrLines' in error && Array.isArray(error.stderrLines) && 'title' in error && 'type' in error) { // This is a DevServerError with parsed stderr - this.logger?.error(messages.getMessage('error.dev-server-failed', [error.title])); + this.warn(messages.getMessage('error.dev-server-failed', [error.title])); this.proxyServer?.setActiveDevServerError(error); } else { // Generic SfError - this.logger?.error(messages.getMessage('error.dev-server-failed', [error.message])); + this.warn(messages.getMessage('error.dev-server-failed', [error.message])); } }); this.devServerManager.on('exit', () => { - this.logger?.debug(messages.getMessage('info.dev-server-exit')); + this.logger?.debug('Dev server stopped'); }); - this.devServerManager.start(); + await this.devServerManager.start(); // Wait for dev server to be ready devServerUrl = await new Promise((resolve, reject) => { @@ -222,58 +193,32 @@ export default class WebappDev extends SfCommand { // Step 3: Initialize authentication const orgConnection = flags['target-org'].getConnection(undefined); orgUsername = flags['target-org'].getUsername() ?? orgConnection.getUsername() ?? 'unknown'; - this.logger.debug(messages.getMessage('info.initializing-auth', [orgUsername])); - const authManager = new AuthManager(orgUsername, this.logger); + this.logger.debug(`Initializing authentication for org: ${orgUsername}`); + const authManager = new AuthManager(orgUsername); await authManager.initialize(); // Step 4: Start proxy server - this.logger.debug(messages.getMessage('info.starting-proxy', [String(flags.port)])); + this.logger.debug(`Starting proxy server on port ${flags.port}...`); const salesforceInstanceUrl = orgConnection.instanceUrl; this.proxyServer = new ProxyServer({ devServerUrl, salesforceInstanceUrl, port: flags.port, authManager, - debug: flags.debug, }); await this.proxyServer.start(); const proxyUrl = this.proxyServer.getProxyUrl(); - this.logger.debug(messages.getMessage('info.proxy-running', [proxyUrl])); - - // Initialize global error capture AFTER proxy server is ready - // This ensures this.proxyServer is available when errors are captured - this.errorCapture = GlobalErrorCapture.getInstance({ - captureExceptions: true, - captureRejections: true, - filterNodeModules: true, - filterNodeInternals: true, - exitOnCritical: false, - workspaceRoot: process.cwd(), - onError: (metadata) => { - // Log error when captured - this.logger?.error(`Runtime error captured: ${metadata.type}: ${metadata.message}`); - - // Set the error as active in the proxy server so it's displayed automatically - // on all requests (just like dev server errors) - if (this.proxyServer) { - this.proxyServer.setActiveRuntimeError(metadata); - this.logger?.error('⚠️ Error will be displayed automatically in your browser'); - this.logger?.info('💡 Fix the error and save - the page will auto-refresh to your app'); - } - }, - }); - this.errorCapture.start(); - this.logger.debug('Global error capture enabled'); + 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.log(`✅ Dev server detected at ${url}`); + this.log(messages.getMessage('info.dev-server-detected', [url])); }); this.proxyServer.on('dev-server-down', (url: string) => { - this.log(`⚠️ Dev server unreachable at ${url}`); - this.log(' Start your dev server to continue development'); + this.log(messages.getMessage('warning.dev-server-unreachable-status', [url])); + this.log(messages.getMessage('info.start-dev-server-hint')); }); // Step 5: Check if dev server is reachable (non-blocking warning) @@ -283,8 +228,8 @@ export default class WebappDev extends SfCommand { // Step 6: Open browser if requested if (flags.open) { - this.logger.debug(messages.getMessage('info.opening-browser')); - WebappDev.openBrowser(proxyUrl); + this.logger.debug('Opening browser...'); + await WebappDev.openBrowser(proxyUrl); } // Display usage instructions @@ -364,35 +309,22 @@ export default class WebappDev extends SfCommand { }); if (response.ok) { - this.logger?.info(messages.getMessage('info.dev-server-healthy', [devServerUrl])); + this.log(messages.getMessage('info.dev-server-healthy', [devServerUrl])); } else { - this.logger?.warn( - messages.getMessage('warning.dev-server-not-responding', [devServerUrl, String(response.status)]) - ); + this.warn(messages.getMessage('warning.dev-server-not-responding', [devServerUrl, String(response.status)])); } } catch (error) { // Dev server not reachable - show warning but don't fail - this.logger?.warn(messages.getMessage('warning.dev-server-unreachable', [devServerUrl])); - this.logger?.warn(messages.getMessage('warning.dev-server-start-hint')); + this.warn(messages.getMessage('warning.dev-server-unreachable', [devServerUrl])); + this.warn(messages.getMessage('warning.dev-server-start-hint')); this.logger?.debug(`Dev server check error: ${(error as Error).message}`); } } /** - * Cleanup all resources (proxy, dev server, file watcher, error capture) + * Cleanup all resources (proxy, dev server, file watcher) */ private async cleanup(): Promise { - // Stop global error capture - if (this.errorCapture) { - try { - this.errorCapture.stop(); - this.logger?.debug('Global error capture stopped'); - } catch (error) { - this.logger?.debug(`Failed to stop error capture: ${(error as Error).message}`); - } - this.errorCapture = null; - } - // Stop proxy server if (this.proxyServer) { try { diff --git a/src/config/types.ts b/src/config/types.ts index 18a2028..d0bd85d 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -173,8 +173,6 @@ export type DevServerOptions = { cwd?: string; /** Timeout in milliseconds to wait for dev server to start */ startupTimeout?: number; - /** Enable debug logging */ - debug?: boolean; /** Maximum number of restart attempts on crash */ maxRestarts?: number; }; @@ -190,7 +188,7 @@ export type DevServerEvents = { exit: (code: number | null, signal: string | null) => void; /** Emitted when dev server encounters an error */ error: (error: Error | DevServerError) => void; - /** Emitted when dev server outputs to stdout (in debug mode) */ + /** Emitted when dev server outputs to stdout (when SF_LOG_LEVEL=debug) */ stdout: (data: string) => void; /** Emitted when dev server outputs to stderr */ stderr: (data: string) => void; diff --git a/src/error/ErrorHandler.ts b/src/error/ErrorHandler.ts index ecec39a..f64b6bc 100644 --- a/src/error/ErrorHandler.ts +++ b/src/error/ErrorHandler.ts @@ -411,147 +411,7 @@ export class ErrorHandler { return new SfError(`${context}: ${sanitizedMessage}`, 'UnexpectedError', [ 'This is an unexpected error', 'Please try again', - 'If the problem persists, check the command logs with --debug flag', + 'If the problem persists, check the command logs with SF_LOG_LEVEL=debug', ]); } - - /** - * Create an error for runtime uncaught exceptions - * - * @param errorMessage - The error message - * @param location - File location where error occurred - * @returns SfError with runtime error details - */ - public static createRuntimeError(errorMessage: string, location?: string): SfError { - const locationInfo = location ? ` at ${location}` : ''; - return new SfError(`Runtime error occurred${locationInfo}: ${errorMessage}`, WebAppErrorCode.RUNTIME_ERROR, [ - 'A runtime error occurred in the application', - 'Check the error details and stack trace for more information', - 'Review recent code changes that might have caused this error', - 'Use the error report to debug the issue', - ]); - } - - /** - * Create an error for uncaught exceptions - * - * @param error - The uncaught exception - * @returns SfError with exception details - */ - public static createUncaughtExceptionError(error: Error): SfError { - const sanitizedMessage = ErrorHandler.sanitizeErrorMessage(error.message); - return new SfError(`Uncaught exception: ${error.name}: ${sanitizedMessage}`, WebAppErrorCode.UNCAUGHT_EXCEPTION, [ - 'An uncaught exception occurred in the application', - 'This indicates a critical error that was not properly handled', - 'Check the stack trace to identify the source of the error', - 'Consider adding proper error handling in the affected code', - ]); - } - - /** - * Create an error for unhandled promise rejections - * - * @param reason - The rejection reason - * @returns SfError with rejection details - */ - public static createUnhandledRejectionError(reason: unknown): SfError { - const message = reason instanceof Error ? reason.message : String(reason); - const sanitizedMessage = ErrorHandler.sanitizeErrorMessage(message); - - return new SfError(`Unhandled promise rejection: ${sanitizedMessage}`, WebAppErrorCode.UNHANDLED_REJECTION, [ - 'A promise was rejected but no error handler was attached', - 'Add .catch() handlers to your promises or use try-catch with async/await', - 'Check the stack trace to find the unhandled promise', - 'Ensure all async operations have proper error handling', - ]); - } - - /** - * Get context-aware suggestions based on error type and message - * - * @param error - Error object - * @returns Array of helpful suggestions - */ - public static getSuggestionsForError(error: Error): string[] { - const suggestions: string[] = []; - const errorMessage = error.message.toLowerCase(); - const errorCode = (error as NodeJS.ErrnoException).code; - - // Network/Connection errors - if (errorCode === 'ECONNREFUSED' || errorMessage.includes('connection refused')) { - suggestions.push('The server or service is not running or not reachable'); - suggestions.push('Check if your dev server is started'); - suggestions.push('Verify the server URL and port are correct'); - } - - // Module/Import errors - if (error.name === 'MODULE_NOT_FOUND' || errorMessage.includes('cannot find module')) { - suggestions.push('A required module or package is missing'); - suggestions.push("Run 'npm install' or 'yarn install' to install dependencies"); - suggestions.push('Check if the module name is spelled correctly'); - suggestions.push('Verify the module is listed in package.json dependencies'); - } - - // Syntax errors - if (error.name === 'SyntaxError') { - suggestions.push('There is a syntax error in your code'); - suggestions.push('Check the file and line number in the stack trace'); - suggestions.push('Look for missing brackets, quotes, or semicolons'); - suggestions.push('Ensure your code follows proper JavaScript/TypeScript syntax'); - } - - // Type errors - if (error.name === 'TypeError') { - suggestions.push('An operation was performed on an incorrect type'); - suggestions.push('Check if variables are properly initialized before use'); - suggestions.push('Verify function arguments are of the expected type'); - suggestions.push('Look for null or undefined values in the stack trace'); - } - - // Reference errors - if (error.name === 'ReferenceError') { - suggestions.push('A variable or function was used before being declared'); - suggestions.push('Check for typos in variable or function names'); - suggestions.push('Ensure variables are in scope where they are used'); - suggestions.push('Verify imports are correct at the top of the file'); - } - - // Authentication errors - if (errorMessage.includes('auth') || errorMessage.includes('token') || errorMessage.includes('unauthorized')) { - suggestions.push('Authentication may have expired or failed'); - suggestions.push("Try re-authenticating with 'sf org login web'"); - suggestions.push('Check your Salesforce org credentials'); - } - - // File system errors - if (errorCode === 'ENOENT' || errorMessage.includes('no such file')) { - suggestions.push('A required file or directory does not exist'); - suggestions.push('Check if the file path is correct'); - suggestions.push('Ensure the file was not deleted or moved'); - } - - // Permission errors - if (errorCode === 'EACCES' || errorMessage.includes('permission denied')) { - suggestions.push('Permission denied to access a file or resource'); - suggestions.push('Check file permissions'); - suggestions.push('You may need elevated privileges for this operation'); - } - - // Port errors - if (errorCode === 'EADDRINUSE' || errorMessage.includes('address already in use')) { - suggestions.push('The port is already in use by another process'); - suggestions.push('Try using a different port with the --port flag'); - suggestions.push('Stop the process using the port or restart your system'); - } - - // Generic fallback - if (suggestions.length === 0) { - suggestions.push('Review the error message and stack trace for details'); - suggestions.push('Check recent code changes that might have caused this'); - suggestions.push('Try running with --debug flag for more information'); - suggestions.push('Search for similar errors online or in documentation'); - } - - return suggestions; - } } diff --git a/src/error/GlobalErrorCapture.ts b/src/error/GlobalErrorCapture.ts deleted file mode 100644 index bccf9b7..0000000 --- a/src/error/GlobalErrorCapture.ts +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright 2025, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { EventEmitter } from 'node:events'; -import { Logger } from '../utils/Logger.js'; -import type { ErrorMetadata, ErrorSeverity, GlobalErrorCaptureOptions } from './types.js'; -import { StackTraceFormatter } from './StackTraceFormatter.js'; - -/** - * GlobalErrorCapture implements a singleton pattern for capturing and handling - * all uncaught exceptions and unhandled promise rejections in the application. - * - * Features: - * - Captures uncaught exceptions - * - Captures unhandled promise rejections - * - Extracts comprehensive error metadata - * - Formats stack traces - * - Emits events for error consumers - * - Prevents duplicate handler registration - * - Graceful error handling and logging - * - * Usage: - * ```typescript - * const capture = GlobalErrorCapture.getInstance({ - * onError: (metadata) => { - * // Handle error (e.g., display in UI, log to file) - * } - * }); - * capture.start(); - * - * // Later... - * capture.stop(); - * ``` - * - * Events: - * - 'error': Emitted when an error is captured - * - 'critical': Emitted for critical errors - */ -export class GlobalErrorCapture extends EventEmitter { - private static instance: GlobalErrorCapture | null = null; - private readonly options: Omit, 'onError'> & { - onError?: (metadata: ErrorMetadata) => void; - }; - private readonly logger: Logger; - private readonly formatter: StackTraceFormatter; - private isStarted = false; - private lastError: ErrorMetadata | null = null; - - // Handler references for cleanup - private readonly boundExceptionHandler: (error: Error) => void; - private readonly boundRejectionHandler: (reason: unknown, promise: Promise) => void; - - private constructor(options: GlobalErrorCaptureOptions = {}) { - super(); - - this.options = { - captureExceptions: options.captureExceptions ?? true, - captureRejections: options.captureRejections ?? true, - filterNodeModules: options.filterNodeModules ?? true, - filterNodeInternals: options.filterNodeInternals ?? true, - exitOnCritical: options.exitOnCritical ?? false, - onError: options.onError, - workspaceRoot: options.workspaceRoot ?? process.cwd(), - }; - - this.logger = new Logger(false); - this.formatter = new StackTraceFormatter({ - filterNodeModules: this.options.filterNodeModules, - filterNodeInternals: this.options.filterNodeInternals, - workspaceRoot: this.options.workspaceRoot, - }); - - // Bind handlers for proper cleanup - this.boundExceptionHandler = this.handleUncaughtException.bind(this); - this.boundRejectionHandler = this.handleUnhandledRejection.bind(this); - } - - /** - * Get or create the singleton instance - * - * @param options - Configuration options (only used on first call) - * @returns GlobalErrorCapture instance - */ - public static getInstance(options?: GlobalErrorCaptureOptions): GlobalErrorCapture { - if (!GlobalErrorCapture.instance) { - GlobalErrorCapture.instance = new GlobalErrorCapture(options); - } - return GlobalErrorCapture.instance; - } - - /** - * Reset the singleton instance (useful for testing) - */ - public static resetInstance(): void { - if (GlobalErrorCapture.instance) { - GlobalErrorCapture.instance.stop(); - GlobalErrorCapture.instance = null; - } - } - - /** - * Check if an error is an intentional exit (e.g., oclif exit, Ctrl+C) - * - * @param error - Error to check - * @returns True if this is an intentional exit - */ - private static isIntentionalExit(error: Error): boolean { - // Check for oclif exit errors (EEXIT) - if ('code' in error && error.code === 'EEXIT') { - return true; - } - - // Check for oclif property indicating controlled exit - type OclifError = Error & { oclif?: { exit?: number } }; - if ( - 'oclif' in error && - typeof (error as OclifError).oclif === 'object' && - 'exit' in ((error as OclifError).oclif ?? {}) - ) { - return true; - } - - // Check for skipOclifErrorHandling flag - if ('skipOclifErrorHandling' in error) { - return true; - } - - // Check error message patterns for intentional exits - const message = error.message ?? ''; - if (message.includes('EEXIT') || message.includes('SIGINT') || message.includes('SIGTERM')) { - return true; - } - - return false; - } - - /** - * Start capturing global errors - */ - public start(): void { - if (this.isStarted) { - this.logger.warn('GlobalErrorCapture is already started'); - return; - } - - if (this.options.captureExceptions) { - process.on('uncaughtException', this.boundExceptionHandler); - this.logger.debug('Registered uncaughtException handler'); - } - - if (this.options.captureRejections) { - process.on('unhandledRejection', this.boundRejectionHandler); - this.logger.debug('Registered unhandledRejection handler'); - } - - this.isStarted = true; - this.logger.debug('GlobalErrorCapture started'); - } - - /** - * Stop capturing global errors and cleanup handlers - */ - public stop(): void { - if (!this.isStarted) { - return; - } - - if (this.options.captureExceptions) { - process.off('uncaughtException', this.boundExceptionHandler); - } - - if (this.options.captureRejections) { - process.off('unhandledRejection', this.boundRejectionHandler); - } - - this.isStarted = false; - this.logger.debug('GlobalErrorCapture stopped'); - } - - /** - * Get the last captured error - * - * @returns Last error metadata or null - */ - public getLastError(): ErrorMetadata | null { - return this.lastError; - } - - /** - * Clear the last error - */ - public clearLastError(): void { - this.lastError = null; - } - - /** - * Manually capture an error with optional context - * - * @param error - Error to capture - * @param context - Error context - * @returns Error metadata - */ - public captureError(error: unknown, context?: string): ErrorMetadata { - const errorObj = error instanceof Error ? error : new Error(String(error)); - return this.captureErrorMetadata(errorObj, { - context, - severity: 'error', - isUnhandledRejection: false, - }); - } - - /** - * Get capture statistics - * - * @returns Capture statistics - */ - public getStats(): { - isStarted: boolean; - hasLastError: boolean; - captureExceptions: boolean; - captureRejections: boolean; - } { - return { - isStarted: this.isStarted, - hasLastError: this.lastError !== null, - captureExceptions: this.options.captureExceptions, - captureRejections: this.options.captureRejections, - }; - } - - /** - * Handle uncaught exceptions - * - * @param error - Uncaught exception - */ - private handleUncaughtException(error: Error): void { - try { - // Ignore intentional exits (oclif exit errors, Ctrl+C, etc.) - if (GlobalErrorCapture.isIntentionalExit(error)) { - this.logger.debug('Ignoring intentional exit signal'); - return; - } - - const metadata = this.captureErrorMetadata(error, { - context: 'Uncaught Exception', - severity: 'critical', - isUnhandledRejection: false, - }); - - this.processError(metadata); - } catch (captureError) { - // Fallback error handling if capture itself fails - this.logger.error( - `Failed to capture uncaught exception: ${ - captureError instanceof Error ? captureError.message : String(captureError) - }` - ); - this.logger.error(`Original error: ${error.message}`); - } - } - - /** - * Handle unhandled promise rejections - * - * @param reason - Rejection reason - */ - private handleUnhandledRejection(reason: unknown): void { - try { - // Convert reason to Error if it's not already - const error = reason instanceof Error ? reason : new Error(String(reason)); - - // Ignore intentional exits - if (GlobalErrorCapture.isIntentionalExit(error)) { - this.logger.debug('Ignoring intentional exit signal from promise rejection'); - return; - } - - const metadata = this.captureErrorMetadata(error, { - context: 'Unhandled Promise Rejection', - severity: 'error', - isUnhandledRejection: true, - }); - - this.processError(metadata); - } catch (captureError) { - // Fallback error handling - this.logger.error( - `Failed to capture unhandled rejection: ${ - captureError instanceof Error ? captureError.message : String(captureError) - }` - ); - this.logger.error(`Original rejection reason: ${String(reason)}`); - } - } - - /** - * Capture comprehensive error metadata - * - * @param error - Error object - * @param options - Additional metadata options - * @returns Error metadata - */ - private captureErrorMetadata( - error: Error, - options: { - context?: string; - severity?: ErrorSeverity; - isUnhandledRejection: boolean; - } - ): ErrorMetadata { - // Capture memory usage - const memUsage = process.memoryUsage(); - - // Format stack trace - const formattedStack = this.formatter.format(error.stack ?? 'No stack trace available'); - - const metadata: ErrorMetadata = { - type: error.name ?? 'Error', - message: error.message ?? 'Unknown error', - stack: error.stack ?? 'No stack trace available', - formattedStack, - timestamp: new Date().toISOString(), - severity: options.severity ?? 'error', - nodeVersion: process.version, - platform: process.platform, - pid: process.pid, - memoryUsage: { - heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024), - heapTotalMB: Math.round(memUsage.heapTotal / 1024 / 1024), - rssMB: Math.round(memUsage.rss / 1024 / 1024), - externalMB: Math.round(memUsage.external / 1024 / 1024), - }, - code: (error as NodeJS.ErrnoException).code, - context: options.context, - isUnhandledRejection: options.isUnhandledRejection, - originalError: error, - }; - - return metadata; - } - - /** - * Process captured error (log, emit events, call callbacks) - * - * @param metadata - Error metadata - */ - private processError(metadata: ErrorMetadata): void { - // Store as last error - this.lastError = metadata; - - // Log the error - this.logError(metadata); - - // Emit events only if there are listeners (EventEmitter throws if 'error' event has no listeners) - if (this.listenerCount('error') > 0) { - this.emit('error', metadata); - } - if (metadata.severity === 'critical' && this.listenerCount('critical') > 0) { - this.emit('critical', metadata); - } - - // Call custom error handler if provided - if (this.options.onError) { - try { - this.options.onError(metadata); - } catch (handlerError) { - this.logger.error( - `Error in custom error handler: ${ - handlerError instanceof Error ? handlerError.message : String(handlerError) - }` - ); - } - } - - // Exit on critical errors if configured - if (this.options.exitOnCritical && metadata.severity === 'critical') { - this.logger.error('Exiting due to critical error'); - // Give some time for async operations to complete - setTimeout(() => { - process.exit(1); - }, 1000); - } - } - - /** - * Log error with formatted output - * - * @param metadata - Error metadata - */ - private logError(metadata: ErrorMetadata): void { - const severityLabel = metadata.severity.toUpperCase(); - const prefix = metadata.isUnhandledRejection ? '[UNHANDLED REJECTION]' : '[UNCAUGHT EXCEPTION]'; - - this.logger.error(`${prefix} ${severityLabel}: ${metadata.type}: ${metadata.message}`); - this.logger.error(`Timestamp: ${metadata.timestamp}`); - this.logger.error( - `Location: ${StackTraceFormatter.extractErrorLocation(metadata.originalError as Error) ?? 'unknown'}` - ); - this.logger.error(`Memory: ${metadata.memoryUsage.heapUsedMB}MB / ${metadata.memoryUsage.heapTotalMB}MB heap`); - - if (metadata.code) { - this.logger.error(`Error Code: ${metadata.code}`); - } - - // Log formatted stack trace - this.logger.error('\nStack Trace:'); - this.logger.error(metadata.formattedStack.text); - - if (metadata.formattedStack.filteredCount > 0) { - this.logger.error(`\n(${metadata.formattedStack.filteredCount} frames filtered)`); - } - } -} diff --git a/src/error/StackTraceFormatter.ts b/src/error/StackTraceFormatter.ts deleted file mode 100644 index 30eefc5..0000000 --- a/src/error/StackTraceFormatter.ts +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright 2025, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { relative, isAbsolute } from 'node:path'; -import type { StackFrame, FormattedStackTrace, StackTraceFormatterOptions } from './types.js'; - -/** - * StackTraceFormatter parses and formats V8 stack traces into - * developer-friendly, syntax-highlighted output for both HTML and terminal display. - * - * Features: - * - Parses V8 stack trace format - * - Filters node_modules and Node.js internals (configurable) - * - Syntax highlighting (file paths, line numbers, function names) - * - Relative path display from workspace root - * - HTML and plain text output - * - Smart truncation for long paths - */ -export class StackTraceFormatter { - /** - * Stack frame regex pattern for V8 stack traces - * Matches patterns like: - * - "at functionName (/path/to/file.ts:10:5)" - * - "at /path/to/file.ts:10:5" - * - "at async functionName (/path/to/file.ts:10:5)" - */ - private static readonly STACK_FRAME_REGEX = /^\s*at\s+(?:(async)\s+)?(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/; - - private readonly options: Required; - - public constructor(options: StackTraceFormatterOptions = {}) { - this.options = { - filterNodeModules: options.filterNodeModules ?? true, - filterNodeInternals: options.filterNodeInternals ?? true, - maxFrames: options.maxFrames ?? 50, - workspaceRoot: options.workspaceRoot ?? process.cwd(), - enableHtmlFormatting: options.enableHtmlFormatting ?? true, - }; - } - - /** - * Create a formatted stack trace from an Error object - * - * @param error - Error object - * @returns Formatted stack trace - */ - public static formatError(error: Error, options?: StackTraceFormatterOptions): FormattedStackTrace { - const formatter = new StackTraceFormatter(options); - return formatter.format(error.stack ?? 'No stack trace available'); - } - - /** - * Extract just the file location from an error for quick display - * - * @param error - Error object - * @returns File location string (e.g., "file.ts:10:5") - */ - public static extractErrorLocation(error: Error): string | null { - if (!error.stack) return null; - - const formatter = new StackTraceFormatter(); - const lines = error.stack.split('\n'); - - // Try to find the first non-internal frame - for (const line of lines.slice(1)) { - const frame = StackTraceFormatter.parseStackFrame(line); - if (frame && !frame.isNodeInternal) { - return `${formatter.getDisplayPath(frame.fileName)}:${frame.lineNumber}:${frame.columnNumber}`; - } - } - - return null; - } - - /** - * Parse a single stack frame line - * - * @param line - Stack frame line - * @returns Parsed stack frame or null if invalid - */ - private static parseStackFrame(line: string): StackFrame | null { - const match = line.match(StackTraceFormatter.STACK_FRAME_REGEX); - if (!match) return null; - - const [, , functionName, fileName, lineNumber, columnNumber] = match; - - const frame: StackFrame = { - functionName: functionName?.trim() || 'anonymous', - fileName: fileName.trim(), - lineNumber: parseInt(lineNumber, 10), - columnNumber: parseInt(columnNumber, 10), - isNodeModule: fileName.includes('node_modules'), - isNodeInternal: fileName.startsWith('node:') || fileName.startsWith('internal/'), - raw: line, - }; - - return frame; - } - - /** - * Escape HTML special characters - * - * @param text - Text to escape - * @returns Escaped HTML - */ - private static escapeHtml(text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - /** - * Format a stack trace string into a structured, formatted output - * - * @param stackTrace - Raw stack trace string - * @returns Formatted stack trace with HTML and text representations - */ - public format(stackTrace: string): FormattedStackTrace { - const lines = stackTrace.split('\n'); - const frames: StackFrame[] = []; - let filteredCount = 0; - - // First line is usually the error message, skip it - const stackLines = lines.slice(1); - - for (const line of stackLines) { - const frame = StackTraceFormatter.parseStackFrame(line); - if (!frame) continue; - - // Apply filters - if (this.shouldFilterFrame(frame)) { - filteredCount++; - continue; - } - - frames.push(frame); - - // Respect max frames limit - if (frames.length >= this.options.maxFrames) { - filteredCount += stackLines.length - frames.length - filteredCount; - break; - } - } - - return { - html: this.options.enableHtmlFormatting ? this.formatAsHtml(frames) : this.formatAsText(frames), - text: this.formatAsText(frames), - frames, - filteredCount, - }; - } - - /** - * Determine if a frame should be filtered based on options - * - * @param frame - Stack frame to check - * @returns True if frame should be filtered out - */ - private shouldFilterFrame(frame: StackFrame): boolean { - if (this.options.filterNodeModules && frame.isNodeModule) { - return true; - } - if (this.options.filterNodeInternals && frame.isNodeInternal) { - return true; - } - return false; - } - - /** - * Format frames as HTML with syntax highlighting - * - * @param frames - Stack frames to format - * @returns HTML formatted stack trace - */ - private formatAsHtml(frames: StackFrame[]): string { - if (frames.length === 0) { - return '
No stack frames available
'; - } - - return frames - .map((frame, index) => { - const displayPath = this.getDisplayPath(frame.fileName); - - return `
- ${index + 1}. - ${StackTraceFormatter.escapeHtml(frame.functionName)} - at - - ${StackTraceFormatter.escapeHtml( - displayPath - )}:${ - frame.lineNumber - }:${frame.columnNumber} - -
`; - }) - .join('\n'); - } - - /** - * Format frames as plain text - * - * @param frames - Stack frames to format - * @returns Plain text formatted stack trace - */ - private formatAsText(frames: StackFrame[]): string { - if (frames.length === 0) { - return 'No stack frames available'; - } - - return frames - .map((frame, index) => { - const displayPath = this.getDisplayPath(frame.fileName); - const padding = ' '.repeat(String(frames.length).length - String(index + 1).length); - return ` ${padding}${index + 1}. ${frame.functionName} at ${displayPath}:${frame.lineNumber}:${ - frame.columnNumber - }`; - }) - .join('\n'); - } - - /** - * Get display path for a file (relative to workspace if possible) - * - * @param filePath - Absolute file path - * @returns Display path (relative or truncated) - */ - private getDisplayPath(filePath: string): string { - // Handle Node.js internals - if (filePath.startsWith('node:') || filePath.startsWith('internal/')) { - return filePath; - } - - // Try to make relative to workspace root - if (isAbsolute(filePath) && this.options.workspaceRoot) { - try { - const rel = relative(this.options.workspaceRoot, filePath); - // Only use relative path if it doesn't start with .. (i.e., it's within workspace) - if (!rel.startsWith('..')) { - return rel; - } - } catch { - // Fall through to absolute path - } - } - - // Truncate long absolute paths from the left - if (filePath.length > 80) { - return '...' + filePath.slice(-77); - } - - return filePath; - } -} diff --git a/src/error/types.ts b/src/error/types.ts deleted file mode 100644 index 667d9cc..0000000 --- a/src/error/types.ts +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2025, 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. - */ - -/** - * Represents a parsed stack frame from a V8 stack trace - */ -export type StackFrame = { - /** - * Function name (e.g., "myFunction" or "anonymous") - */ - functionName: string; - /** - * File path where the error occurred - */ - fileName: string; - /** - * Line number in the file - */ - lineNumber: number; - /** - * Column number in the line - */ - columnNumber: number; - /** - * Whether this frame is from node_modules - */ - isNodeModule: boolean; - /** - * Whether this frame is from Node.js internals - */ - isNodeInternal: boolean; - /** - * Original raw stack line - */ - raw: string; -}; - -/** - * Formatted stack trace with both HTML and plain text representations - */ -export type FormattedStackTrace = { - /** - * HTML formatted stack trace with syntax highlighting - */ - html: string; - /** - * Plain text formatted stack trace - */ - text: string; - /** - * Parsed stack frames - */ - frames: StackFrame[]; - /** - * Number of filtered frames (node_modules, internals) - */ - filteredCount: number; -}; - -/** - * Error severity levels - */ -export type ErrorSeverity = 'critical' | 'error' | 'warning'; - -/** - * Comprehensive error metadata captured from runtime errors - */ -export type ErrorMetadata = { - /** - * Error type/name (e.g., "TypeError", "ReferenceError") - */ - type: string; - /** - * Error message - */ - message: string; - /** - * Raw stack trace string - */ - stack: string; - /** - * Formatted stack trace - */ - formattedStack: FormattedStackTrace; - /** - * ISO timestamp when error occurred - */ - timestamp: string; - /** - * Error severity level - */ - severity: ErrorSeverity; - /** - * Node.js version - */ - nodeVersion: string; - /** - * Operating system platform - */ - platform: string; - /** - * Process ID - */ - pid: number; - /** - * Memory usage at time of error - */ - memoryUsage: { - heapUsedMB: number; - heapTotalMB: number; - rssMB: number; - externalMB: number; - }; - /** - * Error code if available - */ - code?: string; - /** - * Context about where error occurred - */ - context?: string; - /** - * Whether this was an unhandled rejection - */ - isUnhandledRejection: boolean; - /** - * Original error object (for debugging) - */ - originalError?: unknown; -}; - -/** - * Data structure for rendering runtime error pages - */ -export type RuntimeErrorPageData = { - /** - * Error type/name - */ - errorType: string; - /** - * Error message - */ - errorMessage: string; - /** - * HTML formatted stack trace - */ - formattedStackHtml: string; - /** - * Plain text stack trace for copying - */ - formattedStackText: string; - /** - * ISO timestamp - */ - timestamp: string; - /** - * Human-readable timestamp - */ - timestampFormatted: string; - /** - * Error severity - */ - severity: ErrorSeverity; - /** - * System metadata - */ - metadata: { - nodeVersion: string; - platform: string; - pid: number; - heapUsedMB: number; - heapTotalMB: number; - rssMB: number; - }; - /** - * Contextual help suggestions - */ - suggestions: string[]; - /** - * Error code if available - */ - errorCode?: string; - /** - * Full error report as JSON string (for export) - */ - errorReportJson: string; -}; - -/** - * Options for configuring global error capture - */ -export type GlobalErrorCaptureOptions = { - /** - * Whether to capture uncaught exceptions - */ - captureExceptions?: boolean; - /** - * Whether to capture unhandled promise rejections - */ - captureRejections?: boolean; - /** - * Whether to filter node_modules from stack traces - */ - filterNodeModules?: boolean; - /** - * Whether to filter Node.js internals from stack traces - */ - filterNodeInternals?: boolean; - /** - * Whether to exit process on critical errors - */ - exitOnCritical?: boolean; - /** - * Custom error handler callback - */ - onError?: ((metadata: ErrorMetadata) => void) | undefined; - /** - * Workspace root path for relative path display - */ - workspaceRoot?: string; -}; - -/** - * Options for stack trace formatting - */ -export type StackTraceFormatterOptions = { - /** - * Filter out node_modules frames - */ - filterNodeModules?: boolean; - /** - * Filter out Node.js internal frames - */ - filterNodeInternals?: boolean; - /** - * Maximum number of frames to display - */ - maxFrames?: number; - /** - * Workspace root for relative path display - */ - workspaceRoot?: string; - /** - * Enable HTML syntax highlighting - */ - enableHtmlFormatting?: boolean; -}; diff --git a/src/proxy/ProxyServer.ts b/src/proxy/ProxyServer.ts index 9a3140d..ef20a05 100644 --- a/src/proxy/ProxyServer.ts +++ b/src/proxy/ProxyServer.ts @@ -20,14 +20,11 @@ import { EventEmitter } from 'node:events'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import httpProxy from 'http-proxy'; -import { SfError } from '@salesforce/core'; +import { Logger, SfError } from '@salesforce/core'; import type { AuthManager } from '../auth/AuthManager.js'; import { ErrorPageRenderer } from '../templates/ErrorPageRenderer.js'; import type { ErrorPageData } from '../templates/ErrorPageRenderer.js'; -import { Logger } from '../utils/Logger.js'; -import type { ErrorMetadata, RuntimeErrorPageData } from '../error/types.js'; import type { DevServerError } from '../config/types.js'; -import { ErrorHandler } from '../error/ErrorHandler.js'; import { RequestRouter } from './RequestRouter.js'; import type { RouterConfig } from './RequestRouter.js'; @@ -55,10 +52,6 @@ export type ProxyServerConfig = { * Optional router configuration */ routerConfig?: RouterConfig; - /** - * Enable debug logging - */ - debug?: boolean; /** * Host to bind to (0.0.0.0 for all interfaces, 127.0.0.1 for localhost only) */ @@ -104,17 +97,16 @@ export type ProxyStats = { */ export class ProxyServer extends EventEmitter { private readonly config: ProxyServerConfig; - private readonly logger: Logger; + private logger: Logger | null = null; private readonly router: RequestRouter; private readonly proxy: httpProxy; private readonly errorPageRenderer: ErrorPageRenderer; private server: Server | null = null; private readonly stats: ProxyStats; - private readonly isCodeBuilder: boolean; + private isCodeBuilder = false; private healthCheckInterval: NodeJS.Timeout | null = null; private devServerStatus: 'unknown' | 'up' | 'down' | 'error' = 'unknown'; private readonly workspaceScript: string; - private activeRuntimeError: ErrorMetadata | null = null; private activeDevServerError: DevServerError | null = null; private errorClearTimeout: NodeJS.Timeout | null = null; private readonly activeConnections: Set = new Set(); @@ -122,7 +114,6 @@ export class ProxyServer extends EventEmitter { public constructor(config: ProxyServerConfig) { super(); // Call EventEmitter constructor this.config = config; - this.logger = new Logger(config.debug ?? false); this.router = new RequestRouter(config.routerConfig); this.errorPageRenderer = new ErrorPageRenderer(); this.workspaceScript = ProxyServer.detectWorkspaceScript(); @@ -135,11 +126,8 @@ export class ProxyServer extends EventEmitter { startTime: new Date(), }; - // Detect Code Builder environment + // Detect Code Builder environment (sync, no logger needed) this.isCodeBuilder = ProxyServer.detectCodeBuilder(); - if (this.isCodeBuilder) { - this.logger.debug('Code Builder environment detected'); - } // Create http-proxy instance this.proxy = httpProxy.createProxyServer({ @@ -153,11 +141,11 @@ export class ProxyServer extends EventEmitter { this.handleProxyError(err, req, res); }); - this.logger.debug('ProxyServer initialized'); - this.logger.debug(` Dev server: ${config.devServerUrl}`); - this.logger.debug(` Salesforce: ${config.salesforceInstanceUrl}`); - this.logger.debug(` Port: ${config.port}`); - this.logger.debug(` Code Builder: ${this.isCodeBuilder}`); + this.logger?.debug('ProxyServer initialized'); + this.logger?.debug(` Dev server: ${config.devServerUrl}`); + this.logger?.debug(` Salesforce: ${config.salesforceInstanceUrl}`); + this.logger?.debug(` Port: ${config.port}`); + this.logger?.debug(` Code Builder: ${this.isCodeBuilder}`); } /** @@ -207,11 +195,11 @@ export class ProxyServer extends EventEmitter { */ public updateDevServerUrl(newDevServerUrl: string): void { if (this.config.devServerUrl === newDevServerUrl) { - this.logger.debug(`Dev server URL unchanged: ${newDevServerUrl}`); + this.logger?.debug(`Dev server URL unchanged: ${newDevServerUrl}`); return; } - this.logger.info(`Updating dev server URL: ${this.config.devServerUrl} → ${newDevServerUrl}`); + this.logger?.info(`Updating dev server URL: ${this.config.devServerUrl} → ${newDevServerUrl}`); this.config.devServerUrl = newDevServerUrl; // Reset dev server status to trigger fresh check @@ -225,7 +213,9 @@ export class ProxyServer extends EventEmitter { // Perform immediate health check this.checkDevServerHealth().catch((error) => { - this.logger.error(`Failed to check dev server health: ${error instanceof Error ? error.message : String(error)}`); + this.logger?.error( + `Failed to check dev server health: ${error instanceof Error ? error.message : String(error)}` + ); }); } @@ -233,6 +223,14 @@ export class ProxyServer extends EventEmitter { * Starts the proxy server */ public async start(): Promise { + // Initialize logger (respects SF_LOG_LEVEL environment variable) + this.logger = await Logger.child('ProxyServer'); + + // Log Code Builder detection (detection happened in constructor) + if (this.isCodeBuilder) { + this.logger.debug('Code Builder environment detected'); + } + if (this.server) { throw new SfError('Proxy server is already running', 'ProxyAlreadyRunning'); } @@ -243,7 +241,7 @@ export class ProxyServer extends EventEmitter { this.server = createServer((req, res) => { this.handleRequest(req, res).catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Request handling error: ${errorMessage}`); + this.logger?.error(`Request handling error: ${errorMessage}`); this.stats.errors++; }); }); @@ -254,7 +252,7 @@ export class ProxyServer extends EventEmitter { this.handleWebSocketUpgrade(req, socket, head); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`WebSocket upgrade error: ${errorMessage}`); + this.logger?.error(`WebSocket upgrade error: ${errorMessage}`); this.stats.errors++; socket.end(); } @@ -273,9 +271,9 @@ export class ProxyServer extends EventEmitter { // Start listening this.server.listen(this.config.port, host, () => { - this.logger.debug(`Proxy server listening on http://${host}:${this.config.port}`); - this.logger.debug(`Forwarding to dev server: ${this.config.devServerUrl}`); - this.logger.debug(`Forwarding to Salesforce: ${this.config.salesforceInstanceUrl}`); + this.logger?.debug(`Proxy server listening on http://${host}:${this.config.port}`); + this.logger?.debug(`Forwarding to dev server: ${this.config.devServerUrl}`); + this.logger?.debug(`Forwarding to Salesforce: ${this.config.salesforceInstanceUrl}`); // Start periodic health check this.startHealthCheck(); @@ -333,9 +331,6 @@ export class ProxyServer extends EventEmitter { this.errorClearTimeout = null; } - // Clear active runtime error - this.activeRuntimeError = null; - // Destroy all active connections to force immediate shutdown for (const socket of this.activeConnections) { socket.destroy(); @@ -345,7 +340,7 @@ export class ProxyServer extends EventEmitter { return new Promise((resolve, reject) => { // Set a timeout to force resolve if server.close() hangs const forceCloseTimeout = setTimeout(() => { - this.logger.debug('Proxy server stop timeout, forcing shutdown'); + this.logger?.debug('Proxy server stop timeout, forcing shutdown'); this.server = null; resolve(); }, 2000); // 2 second timeout @@ -355,14 +350,14 @@ export class ProxyServer extends EventEmitter { if (error) { // Don't reject on ENOTCONN or similar - server might already be closing if (error.message.includes('Server is not running')) { - this.logger.debug('Proxy server already stopped'); + this.logger?.debug('Proxy server already stopped'); this.server = null; resolve(); } else { reject(new SfError(`Failed to stop proxy server: ${error.message}`, 'ProxyStopFailed')); } } else { - this.logger.debug('Proxy server stopped'); + this.logger?.debug('Proxy server stopped'); this.server = null; resolve(); } @@ -406,58 +401,6 @@ export class ProxyServer extends EventEmitter { /** * Set an active runtime error that will be displayed on all requests - * - * This makes the error visible automatically without requiring developers - * to navigate to a special URL. The error will be displayed on any page - * until it's cleared or times out. - * - * @param metadata - Error metadata from GlobalErrorCapture - */ - public setActiveRuntimeError(metadata: ErrorMetadata): void { - this.activeRuntimeError = metadata; - this.logger.debug('Runtime error is now active - will be displayed on all requests'); - - // Clear any existing timeout - if (this.errorClearTimeout) { - clearTimeout(this.errorClearTimeout); - } - - // Auto-clear error after 5 minutes (in case developer doesn't fix it) - // This prevents the error page from being stuck indefinitely - this.errorClearTimeout = setTimeout(() => { - this.clearActiveRuntimeError(); - this.logger.debug('Runtime error auto-cleared after timeout'); - }, 5 * 60 * 1000); // 5 minutes - } - - /** - * Clear the active runtime error - * - * This should be called when the error is fixed or no longer relevant. - * After clearing, normal proxying will resume. - */ - public clearActiveRuntimeError(): void { - if (this.activeRuntimeError) { - this.logger.debug('Runtime error cleared - resuming normal operation'); - this.activeRuntimeError = null; - } - - if (this.errorClearTimeout) { - clearTimeout(this.errorClearTimeout); - this.errorClearTimeout = null; - } - } - - /** - * Check if there's an active runtime error - * - * @returns True if there's an active runtime error - */ - public hasActiveRuntimeError(): boolean { - return this.activeRuntimeError !== null; - } - - /** * Set an active dev server error that will be displayed to the user * This is shown when the dev server process fails to start or crashes with errors * @@ -466,7 +409,7 @@ export class ProxyServer extends EventEmitter { public setActiveDevServerError(error: DevServerError): void { this.activeDevServerError = error; this.devServerStatus = 'error'; - this.logger.debug(`Dev server error is now active: ${error.title}`); + this.logger?.debug(`Dev server error is now active: ${error.title}`); } /** @@ -475,7 +418,7 @@ export class ProxyServer extends EventEmitter { */ public clearActiveDevServerError(): void { if (this.activeDevServerError) { - this.logger.debug('Dev server error cleared - dev server recovered'); + this.logger?.debug('Dev server error cleared - dev server recovered'); this.activeDevServerError = null; // Status will be updated by health check } @@ -490,97 +433,6 @@ export class ProxyServer extends EventEmitter { return this.activeDevServerError !== null; } - /** - * Serve a runtime error page to the browser - * - * This method formats and serves a beautiful error page when runtime errors occur - * - * @param metadata - Error metadata from GlobalErrorCapture - * @param res - HTTP response object - */ - public serveRuntimeErrorPage(metadata: ErrorMetadata, res: ServerResponse): void { - try { - // Format timestamp - const timestamp = new Date(metadata.timestamp); - const timestampFormatted = timestamp.toLocaleString('en-US', { - dateStyle: 'medium', - timeStyle: 'medium', - }); - - // Get context-aware suggestions - const suggestions = ErrorHandler.getSuggestionsForError(metadata.originalError as Error); - - // Prepare error report JSON - const errorReport = { - type: metadata.type, - message: metadata.message, - code: metadata.code, - timestamp: metadata.timestamp, - severity: metadata.severity, - stack: metadata.formattedStack.text, - frames: metadata.formattedStack.frames.map((frame) => ({ - function: frame.functionName, - file: frame.fileName, - line: frame.lineNumber, - column: frame.columnNumber, - })), - system: { - nodeVersion: metadata.nodeVersion, - platform: metadata.platform, - pid: metadata.pid, - memory: metadata.memoryUsage, - }, - context: metadata.context, - isUnhandledRejection: metadata.isUnhandledRejection, - }; - - // Prepare runtime error page data - const pageData: RuntimeErrorPageData = { - errorType: metadata.type, - errorMessage: metadata.message, - formattedStackHtml: metadata.formattedStack.html, - formattedStackText: metadata.formattedStack.text, - timestamp: metadata.timestamp, - timestampFormatted, - severity: metadata.severity, - metadata: { - nodeVersion: metadata.nodeVersion, - platform: metadata.platform, - pid: metadata.pid, - heapUsedMB: metadata.memoryUsage.heapUsedMB, - heapTotalMB: metadata.memoryUsage.heapTotalMB, - rssMB: metadata.memoryUsage.rssMB, - }, - suggestions, - errorCode: metadata.code, - errorReportJson: JSON.stringify(errorReport, null, 2), - }; - - // Render the error page - const html = this.errorPageRenderer.renderRuntimeError(pageData); - - // Send response - res.writeHead(500, { - 'Content-Type': 'text/html', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - }); - res.end(html); - - this.logger.debug(`Served runtime error page: ${metadata.type}: ${metadata.message}`); - } catch (renderError) { - // Critical error: template rendering failed - const errorMessage = renderError instanceof Error ? renderError.message : String(renderError); - this.logger.error(`CRITICAL: Failed to render error page template: ${errorMessage}`); - - // Send plain text error as last resort - res.writeHead(500, { - 'Content-Type': 'text/plain', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - }); - res.end(`Runtime Error: ${metadata.type}\n\n${metadata.message}\n\nCheck terminal logs for details.`); - } - } - /** * Checks if running in Code Builder environment */ @@ -597,7 +449,7 @@ export class ProxyServer extends EventEmitter { */ public setupGracefulShutdown(onShutdown?: () => void | Promise): () => void { const handleShutdown = async (signal: string): Promise => { - this.logger.debug(`Received ${signal}, shutting down gracefully...`); + this.logger?.debug(`Received ${signal}, shutting down gracefully...`); // Run optional callback if (onShutdown) { @@ -605,7 +457,7 @@ export class ProxyServer extends EventEmitter { await onShutdown(); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); - this.logger.error(`Shutdown callback error: ${errorMessage}`); + this.logger?.error(`Shutdown callback error: ${errorMessage}`); } } @@ -616,7 +468,7 @@ export class ProxyServer extends EventEmitter { const sigintHandler = (): void => { handleShutdown('SIGINT').catch((err) => { const errorMessage = err instanceof Error ? err.message : String(err); - this.logger.error(`SIGINT handler error: ${errorMessage}`); + this.logger?.error(`SIGINT handler error: ${errorMessage}`); process.exit(1); }); }; @@ -624,7 +476,7 @@ export class ProxyServer extends EventEmitter { const sigtermHandler = (): void => { handleShutdown('SIGTERM').catch((err) => { const errorMessage = err instanceof Error ? err.message : String(err); - this.logger.error(`SIGTERM handler error: ${errorMessage}`); + this.logger?.error(`SIGTERM handler error: ${errorMessage}`); process.exit(1); }); }; @@ -658,10 +510,10 @@ export class ProxyServer extends EventEmitter { }); res.end(html); - this.logger.debug('Served dev server error page to browser'); + this.logger?.debug('Served dev server error page to browser'); } catch (err) { // Fallback if rendering fails - this.logger.error(`Failed to render dev server error page: ${err instanceof Error ? err.message : String(err)}`); + this.logger?.error(`Failed to render dev server error page: ${err instanceof Error ? err.message : String(err)}`); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end(`Dev Server Error: ${error.title}\n\n${error.message}\n\nCheck terminal for details.`); } @@ -673,13 +525,13 @@ export class ProxyServer extends EventEmitter { private startHealthCheck(): void { // Initial check this.checkDevServerHealth().catch((error) => { - this.logger.debug(`Initial health check error: ${String(error)}`); + this.logger?.debug(`Initial health check error: ${String(error)}`); }); // Check every 10 seconds this.healthCheckInterval = setInterval(() => { this.checkDevServerHealth().catch((error) => { - this.logger.debug(`Health check error: ${String(error)}`); + this.logger?.debug(`Health check error: ${String(error)}`); }); }, 10_000); // 10 seconds } @@ -698,7 +550,7 @@ export class ProxyServer extends EventEmitter { if (this.devServerStatus !== 'up') { this.devServerStatus = 'up'; this.emit('dev-server-up', this.config.devServerUrl); - this.logger.debug(`Dev server is UP: ${this.config.devServerUrl}`); + this.logger?.debug(`Dev server is UP: ${this.config.devServerUrl}`); // Clear any active dev server error since server is now healthy this.clearActiveDevServerError(); @@ -708,7 +560,7 @@ export class ProxyServer extends EventEmitter { if (this.devServerStatus !== 'down') { this.devServerStatus = 'down'; this.emit('dev-server-down', this.config.devServerUrl); - this.logger.debug(`Dev server is DOWN: ${this.config.devServerUrl}`); + this.logger?.debug(`Dev server is DOWN: ${this.config.devServerUrl}`); } } } @@ -725,7 +577,7 @@ export class ProxyServer extends EventEmitter { // Code Builder requires binding to all interfaces (0.0.0.0) // so it can be accessed from the browser preview if (this.isCodeBuilder) { - this.logger.debug('Code Builder: Binding to 0.0.0.0 (all interfaces)'); + this.logger?.debug('Code Builder: Binding to 0.0.0.0 (all interfaces)'); return '0.0.0.0'; } @@ -741,30 +593,12 @@ export class ProxyServer extends EventEmitter { const url = req.url ?? '/'; const method = req.method ?? 'GET'; - this.logger.debug(`[${method}] ${url}`); - - // Special route to clear active runtime error (for manual clearing) - if (url === '/__clear_error__') { - this.clearActiveRuntimeError(); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end( - '

Error Cleared

Runtime error has been cleared. Return to app

' - ); - return; - } - - // If there's an active runtime error, display it on ALL requests - // This provides automatic error visibility without requiring navigation to a special URL - if (this.activeRuntimeError) { - this.logger.debug('Active runtime error - serving error page'); - this.serveRuntimeErrorPage(this.activeRuntimeError, res); - return; - } + this.logger?.debug(`[${method}] ${url}`); // If there's an active dev server error, display it on ALL requests // This shows parsed stderr and suggestions when dev server fails to start if (this.activeDevServerError) { - this.logger.debug('Active dev server error - serving error page'); + this.logger?.debug('Active dev server error - serving error page'); this.serveDevServerErrorPage(this.activeDevServerError, res); return; } @@ -772,7 +606,7 @@ export class ProxyServer extends EventEmitter { try { // Determine routing const decision = this.router.route(req); - this.logger.debug(`Routing decision: ${decision.target} (${decision.reason})`); + this.logger?.debug(`Routing decision: ${decision.target} (${decision.reason})`); if (decision.target === 'salesforce') { await this.proxySalesforceRequest(req, res); @@ -802,7 +636,7 @@ export class ProxyServer extends EventEmitter { // Prepare target URL const targetUrl = this.prepareSalesforceUrl(req.url ?? '/'); - this.logger.debug(`→ Salesforce: ${targetUrl}`); + this.logger?.debug(`→ Salesforce: ${targetUrl}`); // Proxy the request with auth headers this.proxy.web( @@ -828,7 +662,7 @@ export class ProxyServer extends EventEmitter { // Try to refresh token and retry const recovered = await this.config.authManager.handleAuthError(error); if (recovered) { - this.logger.debug('Token refreshed, retrying request'); + this.logger?.debug('Token refreshed, retrying request'); return this.proxySalesforceRequest(req, res); } } @@ -844,7 +678,7 @@ export class ProxyServer extends EventEmitter { this.stats.devServerRequests++; const url = req.url ?? '/'; - this.logger.debug(`→ Dev Server: ${url}`); + this.logger?.debug(`→ Dev Server: ${url}`); // If dev server is known to be down, serve HTML error page immediately if (this.devServerStatus === 'down') { @@ -891,7 +725,7 @@ export class ProxyServer extends EventEmitter { } catch (error) { // Critical error: template rendering failed const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`CRITICAL: Failed to render dev server error page: ${errorMessage}`); + this.logger?.error(`CRITICAL: Failed to render dev server error page: ${errorMessage}`); // Send plain text error as last resort res.writeHead(503, { 'Content-Type': 'text/plain' }); @@ -908,25 +742,25 @@ export class ProxyServer extends EventEmitter { this.stats.webSocketUpgrades++; const url = req.url ?? '/'; - this.logger.debug(`[WebSocket] Upgrade request: ${url}`); + this.logger?.debug(`[WebSocket] Upgrade request: ${url}`); try { // Determine routing const decision = this.router.route(req); if (!decision.isWebSocket) { - this.logger.warn(`Upgrade request but not detected as WebSocket: ${url}`); + this.logger?.warn(`Upgrade request but not detected as WebSocket: ${url}`); } // WebSocket upgrades typically go to dev server (HMR) if (decision.target === 'devserver') { - this.logger.debug(`→ Dev Server WebSocket: ${url}`); + this.logger?.debug(`→ Dev Server WebSocket: ${url}`); this.proxy.ws(req, socket, head, { target: this.config.devServerUrl, }); } else { // Salesforce WebSocket (rare, but possible for streaming APIs) - this.logger.debug(`→ Salesforce WebSocket: ${url}`); + this.logger?.debug(`→ Salesforce WebSocket: ${url}`); const authHeaders = this.config.authManager.getAuthHeaders(); this.proxy.ws( req, @@ -938,14 +772,14 @@ export class ProxyServer extends EventEmitter { }, (error) => { if (error) { - this.logger.error(`WebSocket proxy error: ${error.message}`); + this.logger?.error(`WebSocket proxy error: ${error.message}`); socket.end(); } } ); } } catch (error) { - this.logger.error(`WebSocket upgrade failed: ${error instanceof Error ? error.message : String(error)}`); + this.logger?.error(`WebSocket upgrade failed: ${error instanceof Error ? error.message : String(error)}`); socket.end(); } } @@ -970,7 +804,7 @@ export class ProxyServer extends EventEmitter { private handleProxyError(error: Error, req: IncomingMessage, res: ServerResponse | NodeJS.Socket): void { this.stats.errors++; const url = req.url ?? '/'; - this.logger.error(`Proxy error for ${url}: ${error.message}`); + this.logger?.error(`Proxy error for ${url}: ${error.message}`); // If response hasn't been sent, send error // Check if res has writeHead method (ServerResponse) vs destroy (Socket) @@ -981,7 +815,6 @@ export class ProxyServer extends EventEmitter { JSON.stringify({ error: errorInfo.error, message: errorInfo.message, - details: this.config.debug ? error.message : undefined, suggestion: errorInfo.suggestion, }) ); @@ -1052,7 +885,7 @@ export class ProxyServer extends EventEmitter { statusCode: 502, error: 'Proxy Error', message: `Failed to proxy request to ${target}`, - suggestion: this.config.debug ? undefined : 'Run with --debug flag for more details', + suggestion: 'Set SF_LOG_LEVEL=debug for more details', }; } @@ -1063,7 +896,7 @@ export class ProxyServer extends EventEmitter { this.stats.errors++; const errorMessage = error instanceof Error ? error.message : String(error); const url = req.url ?? '/'; - this.logger.error(`Request error for ${url}: ${errorMessage}`); + this.logger?.error(`Request error for ${url}: ${errorMessage}`); if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'application/json' }); @@ -1071,7 +904,6 @@ export class ProxyServer extends EventEmitter { JSON.stringify({ error: 'Internal Server Error', message: 'Failed to process request', - details: this.config.debug ? errorMessage : undefined, }) ); } diff --git a/src/proxy/RequestRouter.ts b/src/proxy/RequestRouter.ts index dccc0ac..00eb722 100644 --- a/src/proxy/RequestRouter.ts +++ b/src/proxy/RequestRouter.ts @@ -15,7 +15,6 @@ */ import type { IncomingMessage } from 'node:http'; -import { Logger } from '../utils/Logger.js'; /** * Route decision result @@ -47,10 +46,6 @@ export type RouterConfig = { * Custom path patterns to route to dev server (overrides defaults) */ customDevServerPaths?: string[]; - /** - * Enable debug logging - */ - debug?: boolean; }; /** @@ -74,8 +69,6 @@ export type RouterConfig = { * - Any path not matching Salesforce patterns */ export class RequestRouter { - private readonly logger: Logger; - /** * Default Salesforce API path patterns * These paths will always be routed to Salesforce @@ -136,8 +129,6 @@ export class RequestRouter { ]; public constructor(config: RouterConfig = {}) { - this.logger = new Logger(config.debug ?? false); - // Merge custom paths if provided if (config.customSalesforcePaths) { this.salesforcePaths.push(...config.customSalesforcePaths); @@ -146,10 +137,6 @@ export class RequestRouter { if (config.customDevServerPaths) { this.devServerPaths.unshift(...config.customDevServerPaths); } - - this.logger.debug('RequestRouter initialized with configuration:'); - this.logger.debug(` Salesforce paths: ${this.salesforcePaths.length}`); - this.logger.debug(` Dev server paths: ${this.devServerPaths.length}`); } /** @@ -173,18 +160,12 @@ export class RequestRouter { */ public route(req: IncomingMessage): RouteDecision { const url = req.url ?? '/'; - const method = req.method ?? 'GET'; - - this.logger.debug(`Routing ${method} ${url}`); // Extract just the path without query string const path = url.split('?')[0]; // Check for WebSocket upgrade requests const isWebSocketUpgrade = RequestRouter.isWebSocketUpgrade(req); - if (isWebSocketUpgrade) { - this.logger.debug('→ WebSocket upgrade detected'); - } // Check for dev server-specific paths first (HMR, WebSocket, etc.) for (const devPath of this.devServerPaths) { @@ -194,7 +175,6 @@ export class RequestRouter { reason: `matches dev server path pattern: ${devPath}`, ...(isWebSocketUpgrade && { isWebSocket: true }), }; - this.logger.debug(`→ ${decision.target}: ${decision.reason}`); return decision; } } @@ -207,7 +187,6 @@ export class RequestRouter { reason: `matches Salesforce API path: ${sfPath}`, ...(isWebSocketUpgrade && { isWebSocket: true }), }; - this.logger.debug(`→ ${decision.target}: ${decision.reason}`); return decision; } } @@ -220,7 +199,6 @@ export class RequestRouter { reason: `matches dev server file extension: ${ext}`, ...(isWebSocketUpgrade && { isWebSocket: true }), }; - this.logger.debug(`→ ${decision.target}: ${decision.reason}`); return decision; } } @@ -231,7 +209,6 @@ export class RequestRouter { reason: 'default route (no specific pattern matched)', ...(isWebSocketUpgrade && { isWebSocket: true }), }; - this.logger.debug(`→ ${decision.target}: ${decision.reason}`); return decision; } @@ -281,7 +258,6 @@ export class RequestRouter { public addSalesforcePath(path: string): void { if (!this.salesforcePaths.includes(path)) { this.salesforcePaths.push(path); - this.logger.debug(`Added custom Salesforce path: ${path}`); } } @@ -293,7 +269,6 @@ export class RequestRouter { public addDevServerPath(path: string): void { if (!this.devServerPaths.includes(path)) { this.devServerPaths.unshift(path); // Add to beginning for priority - this.logger.debug(`Added custom dev server path: ${path}`); } } } diff --git a/src/server/DevServerManager.ts b/src/server/DevServerManager.ts index 61e72cd..65a4d07 100644 --- a/src/server/DevServerManager.ts +++ b/src/server/DevServerManager.ts @@ -16,9 +16,8 @@ import { EventEmitter } from 'node:events'; import { spawn, type ChildProcess } from 'node:child_process'; -import { SfError } from '@salesforce/core'; +import { Logger, SfError } from '@salesforce/core'; import type { DevServerOptions, DevServerStatus } from '../config/types.js'; -import { Logger } from '../utils/Logger.js'; import { DevServerErrorParser } from '../error/DevServerErrorParser.js'; /** @@ -63,7 +62,6 @@ const URL_PATTERNS = [...VITE_PATTERNS, ...CRA_PATTERNS, ...NEXTJS_PATTERNS, ... const DEFAULT_OPTIONS = { cwd: process.cwd(), startupTimeout: 30_000, // 30 seconds - debug: false, maxRestarts: 3, } as const; @@ -75,7 +73,6 @@ type DevServerConfig = { explicitUrl?: string; cwd: string; startupTimeout: number; - debug: boolean; maxRestarts: number; }; @@ -88,14 +85,13 @@ type DevServerConfig = { * - Monitors process health and emits lifecycle events * - Handles process cleanup and graceful shutdown * - Supports automatic restart on crash (with retry limits) - * - Provides debug logging for process output + * - Provides debug logging for process output (use SF_LOG_LEVEL=debug) * * @example * ```typescript * const manager = new DevServerManager({ * command: 'npm run dev', * cwd: '/path/to/project', - * debug: true * }); * * manager.on('ready', (url) => { @@ -116,7 +112,7 @@ export class DevServerManager extends EventEmitter { private startupTimer: NodeJS.Timeout | null = null; private isReady = false; private restartCount = 0; - private logger: Logger; + private logger: Logger | null = null; private stderrBuffer: string[] = []; // Buffer to store stderr lines for error parsing private readonly maxStderrLines = 100; // Keep last 100 lines @@ -128,7 +124,6 @@ export class DevServerManager extends EventEmitter { public constructor(options: DevServerOptions) { super(); this.options = { ...DEFAULT_OPTIONS, ...options }; - this.logger = new Logger(this.options.debug); } /** @@ -205,10 +200,13 @@ export class DevServerManager extends EventEmitter { * @throws SfError if process fails to start * @throws SfError if URL is not detected within the timeout period */ - public start(): void { + public async start(): Promise { + // Initialize logger + await this.initLogger(); + // If explicit URL is provided, skip process spawning if (this.options.explicitUrl) { - this.logger.debug(`Using explicit dev server URL: ${this.options.explicitUrl}`); + this.logger?.debug(`Using explicit dev server URL: ${this.options.explicitUrl}`); this.detectedUrl = this.options.explicitUrl; this.isReady = true; this.emit('ready', this.detectedUrl); @@ -224,7 +222,7 @@ export class DevServerManager extends EventEmitter { ); } - this.logger.debug(`Starting dev server with command: ${this.options.command}`); + this.logger?.debug(`Starting dev server with command: ${this.options.command}`); // Parse command into executable and arguments const [cmd, ...args] = DevServerManager.parseCommand(this.options.command); @@ -269,7 +267,7 @@ export class DevServerManager extends EventEmitter { return; } - this.logger.debug('Stopping dev server process...'); + this.logger?.debug('Stopping dev server process...'); // Clear startup timer if (this.startupTimer) { @@ -287,7 +285,7 @@ export class DevServerManager extends EventEmitter { // Setup exit handler const onExit = (): void => { - this.logger.debug('Dev server process stopped'); + this.logger?.debug('Dev server process stopped'); this.process = null; resolve(); }; @@ -300,7 +298,7 @@ export class DevServerManager extends EventEmitter { // Force kill after 3 seconds if still running setTimeout(() => { if (this.process && !this.process.killed) { - this.logger.warn('Dev server did not exit gracefully, forcing kill...'); + this.logger?.warn('Dev server did not exit gracefully, forcing kill...'); this.process.kill('SIGKILL'); } }, 3000); @@ -333,6 +331,16 @@ export class DevServerManager extends EventEmitter { return this.detectedUrl; } + /** + * Initialize the logger (must be called before start) + */ + private async initLogger(): Promise { + if (!this.logger) { + // Logger respects SF_LOG_LEVEL environment variable + this.logger = await Logger.child('DevServerManager'); + } + } + /** * Sets up event handlers for the spawned process * @@ -391,12 +399,10 @@ export class DevServerManager extends EventEmitter { } } - // Log output in debug mode - if (this.options.debug) { - const lines = output.split('\n').filter((line) => line.trim()); - for (const line of lines) { - this.logger.debug(`[Dev Server ${stream}] ${line}`); - } + // 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}`); } // Try to detect URL if not yet ready @@ -429,7 +435,7 @@ export class DevServerManager extends EventEmitter { // Clear stderr buffer on successful start this.stderrBuffer = []; - this.logger.debug(`Dev server detected at: ${url}`); + this.logger?.debug(`Dev server detected at: ${url}`); this.emit('ready', url); } @@ -443,7 +449,7 @@ export class DevServerManager extends EventEmitter { * @param signal Signal that caused exit (if any) */ private handleProcessExit(code: number | null, signal: string | null): void { - this.logger.debug(`Dev server process exited with code ${code ?? 'null'}, signal ${signal ?? 'null'}`); + this.logger?.debug(`Dev server process exited with code ${code ?? 'null'}, signal ${signal ?? 'null'}`); // Clear startup timeout if (this.startupTimer) { @@ -463,8 +469,8 @@ export class DevServerManager extends EventEmitter { const stderrContent = this.stderrBuffer.join('\n'); const parsedError = DevServerErrorParser.parseError(stderrContent, code, signal); - this.logger.error(`Dev server error: ${parsedError.title}`); - this.logger.debug(`Error type: ${parsedError.type}`); + this.logger?.error(`Dev server error: ${parsedError.title}`); + this.logger?.debug(`Error type: ${parsedError.type}`); // Emit parsed error this.emit('error', parsedError); @@ -474,7 +480,7 @@ export class DevServerManager extends EventEmitter { if (shouldRetry && this.restartCount < this.options.maxRestarts) { this.restartCount += 1; - this.logger.warn( + this.logger?.warn( `Dev server crashed, attempting restart (${this.restartCount}/${this.options.maxRestarts})...` ); this.isReady = false; @@ -483,15 +489,13 @@ export class DevServerManager extends EventEmitter { // Restart after a short delay setTimeout(() => { - try { - this.start(); - } catch (error) { + this.start().catch((error: unknown) => { const sfError = error instanceof SfError ? error : new SfError(error instanceof Error ? error.message : String(error), 'DevServerRestartError'); this.emit('error', sfError); - } + }); }, 2000); } @@ -502,25 +506,23 @@ export class DevServerManager extends EventEmitter { // Normal crash handling (no stderr or expected exit) if (!wasExpectedExit && this.isReady && this.restartCount < this.options.maxRestarts) { this.restartCount += 1; - this.logger.warn(`Dev server crashed, attempting restart (${this.restartCount}/${this.options.maxRestarts})...`); + this.logger?.warn(`Dev server crashed, attempting restart (${this.restartCount}/${this.options.maxRestarts})...`); this.isReady = false; this.detectedUrl = null; this.stderrBuffer = []; // Clear buffer // Restart after a short delay setTimeout(() => { - try { - this.start(); - } catch (error) { + this.start().catch((error: unknown) => { const sfError = error instanceof SfError ? error : new SfError(error instanceof Error ? error.message : String(error), 'DevServerRestartError'); this.emit('error', sfError); - } + }); }, 2000); } else if (!wasExpectedExit && this.restartCount >= this.options.maxRestarts) { - this.logger.error('Dev server restart limit reached'); + this.logger?.error('Dev server restart limit reached'); const error = new SfError( 'Dev server crashed and exceeded maximum restart attempts', 'DevServerMaxRestartsExceeded', @@ -541,7 +543,7 @@ export class DevServerManager extends EventEmitter { * @param error The error from the process */ private handleProcessError(error: Error): void { - this.logger.error(`Dev server process error: ${error.message}`); + this.logger?.error(`Dev server process error: ${error.message}`); const sfError = new SfError(`Dev server process error: ${error.message}`, 'DevServerProcessError', [ 'Check that the command is correct in webapp.json', @@ -559,7 +561,7 @@ export class DevServerManager extends EventEmitter { * Kills the process and emits an error */ private handleStartupTimeout(): void { - this.logger.error('Dev server failed to start within timeout period'); + 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`, diff --git a/src/templates/ErrorPageRenderer.ts b/src/templates/ErrorPageRenderer.ts index cf380a5..a6cb446 100644 --- a/src/templates/ErrorPageRenderer.ts +++ b/src/templates/ErrorPageRenderer.ts @@ -17,9 +17,7 @@ import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import type { RuntimeErrorPageData } from '../error/types.js'; import type { DevServerError } from '../config/types.js'; -import { Logger } from '../utils/Logger.js'; export type ErrorPageData = { status: string; @@ -37,24 +35,17 @@ export type ErrorPageData = { */ export class ErrorPageRenderer { private template: string; - private logger: Logger; public constructor() { - this.logger = new Logger(true); // Enable debug for template loading - // 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'); - this.logger.debug('[ErrorPageRenderer] Template loaded successfully'); - this.logger.debug(`[ErrorPageRenderer] Template length: ${this.template.length} chars`); } catch (error) { - this.logger.error('[ErrorPageRenderer] CRITICAL: Failed to load template!'); - this.logger.error(`[ErrorPageRenderer] Error: ${error instanceof Error ? error.message : String(error)}`); - this.logger.error(`[ErrorPageRenderer] Path: ${templatePath}`); - throw new Error('Failed to load error template'); + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to load error template from ${templatePath}: ${errorMessage}`); } } @@ -129,80 +120,6 @@ export class ErrorPageRenderer { ); } - /** - * Render a runtime error page with comprehensive error details - * - * @param data - Runtime error data - * @returns Rendered HTML string - */ - public renderRuntimeError(data: RuntimeErrorPageData): string { - try { - this.logger.debug('[ErrorPageRenderer] Starting renderRuntimeError'); - - const severityLabel = data.severity.toUpperCase(); - - // Build suggestions list (just the
  • items, not the wrapping structure) - const suggestionsList = - data.suggestions.length > 0 - ? data.suggestions.map((s) => `
  • ${ErrorPageRenderer.escapeHtml(s)}
  • `).join('\n') - : '
  • No specific suggestions available. Check the error details and stack trace above.
  • '; - - // Build error code badge if present - const errorCodeBadge = data.errorCode - ? ` ${ErrorPageRenderer.escapeHtml(data.errorCode)}` - : ''; - - // Use default proxy port for emergency commands - const proxyPort = '4545'; - - const html = this.template - // Page metadata - .replace(/\{\{PAGE_TITLE\}\}/g, 'Runtime Error') - .replace(/\{\{META_REFRESH\}\}/g, '') - - // Header - .replace(/\{\{ERROR_TITLE\}\}/g, ErrorPageRenderer.escapeHtml(data.errorType)) - .replace(/\{\{STATUS_CLASS\}\}/g, data.severity) - .replace(/\{\{ERROR_STATUS\}\}/g, `${severityLabel} Error`) - - // Message content - .replace(/\{\{ERROR_MESSAGE_TEXT\}\}/g, ErrorPageRenderer.escapeHtml(data.errorMessage)) - - // Runtime error data - .replace(/\{\{FORMATTED_STACK_HTML\}\}/g, data.formattedStackHtml) - .replace(/\{\{ERROR_TYPE\}\}/g, ErrorPageRenderer.escapeHtml(data.errorType)) - .replace(/\{\{ERROR_CODE_BADGE\}\}/g, errorCodeBadge) - .replace(/\{\{SEVERITY_LABEL\}\}/g, severityLabel) - .replace(/\{\{TIMESTAMP_FORMATTED\}\}/g, data.timestampFormatted) - .replace(/\{\{NODE_VERSION\}\}/g, ErrorPageRenderer.escapeHtml(data.metadata.nodeVersion)) - .replace(/\{\{PLATFORM\}\}/g, ErrorPageRenderer.escapeHtml(data.metadata.platform)) - .replace(/\{\{HEAP_USED_MB\}\}/g, String(data.metadata.heapUsedMB)) - .replace(/\{\{HEAP_TOTAL_MB\}\}/g, String(data.metadata.heapTotalMB)) - .replace(/\{\{PID\}\}/g, String(data.metadata.pid)) - .replace(/\{\{PROXY_PORT\}\}/g, proxyPort) - - // Suggestions - .replace(/\{\{SUGGESTIONS_TITLE\}\}/g, 'How to Fix') - .replace(/\{\{SUGGESTIONS_LIST\}\}/g, suggestionsList) - - // Section visibility (show runtime, hide others) - .replace(/\{\{SIMPLE_SECTION_CLASS\}\}/g, 'hidden') - .replace(/\{\{RUNTIME_SECTION_CLASS\}\}/g, '') - .replace(/\{\{DEV_SERVER_SECTION_CLASS\}\}/g, 'hidden') - .replace(/\{\{SUGGESTIONS_SECTION_CLASS\}\}/g, '') - .replace(/\{\{AUTO_REFRESH_CLASS\}\}/g, 'hidden'); - - this.logger.debug('[ErrorPageRenderer] Successfully rendered runtime error page'); - this.logger.debug(`[ErrorPageRenderer] Output length: ${html.length} chars`); - - return html; - } catch (error) { - this.logger.error('[ErrorPageRenderer] RENDER ERROR:'); - this.logger.error(String(error)); - throw error; - } - } - /** * Render a dev server error page with stderr output and suggestions * @@ -210,59 +127,49 @@ export class ErrorPageRenderer { * @returns Rendered HTML string */ public renderDevServerError(error: DevServerError): string { - try { - this.logger.debug('[ErrorPageRenderer] Starting renderDevServerError'); - this.logger.debug(`[ErrorPageRenderer] Error type: ${error.type}`); - - // Format suggestions list (just the
  • items, structure is in template) - const suggestionsList = error.suggestions.map((s) => `
  • ${ErrorPageRenderer.escapeHtml(s)}
  • `).join('\n'); - - // Format stderr lines with proper escaping (just the text content) - const stderrOutput = error.stderrLines.map((line) => ErrorPageRenderer.escapeHtml(line)).join('\n'); - - // Use default proxy port for emergency commands - const proxyPort = '4545'; - - const html = this.template - // Page metadata - .replace(/\{\{PAGE_TITLE\}\}/g, 'Dev Server Error') - .replace(/\{\{META_REFRESH\}\}/g, '') - - // Header - .replace(/\{\{ERROR_TITLE\}\}/g, ErrorPageRenderer.escapeHtml(error.title)) - .replace(/\{\{STATUS_CLASS\}\}/g, 'error') - .replace(/\{\{ERROR_STATUS\}\}/g, 'Error Detected') - - // Message content - .replace(/\{\{ERROR_MESSAGE_TEXT\}\}/g, ErrorPageRenderer.escapeHtml(error.message)) - - // Dev server error data - .replace(/\{\{STDERR_OUTPUT\}\}/g, stderrOutput) - .replace(/\{\{PROXY_PORT\}\}/g, proxyPort) - - // Suggestions - .replace(/\{\{SUGGESTIONS_TITLE\}\}/g, 'How to Fix This') - .replace(/\{\{SUGGESTIONS_LIST\}\}/g, suggestionsList) - - // Section visibility (show dev server error, hide others) - .replace(/\{\{SIMPLE_SECTION_CLASS\}\}/g, 'hidden') - .replace(/\{\{RUNTIME_SECTION_CLASS\}\}/g, 'hidden') - .replace(/\{\{DEV_SERVER_SECTION_CLASS\}\}/g, '') - .replace(/\{\{SUGGESTIONS_SECTION_CLASS\}\}/g, '') - - // Auto-refresh - .replace(/\{\{AUTO_REFRESH_CLASS\}\}/g, '') - .replace( - /\{\{AUTO_REFRESH_TEXT\}\}/g, - 'This page will auto-refresh every 5 seconds until the dev server starts successfully' - ); - - this.logger.debug('[ErrorPageRenderer] Successfully rendered dev server error page'); - return html; - } catch (renderError) { - this.logger.error('[ErrorPageRenderer] DEV SERVER ERROR RENDER ERROR:'); - this.logger.error(String(renderError)); - throw renderError; - } + // Format suggestions list (just the
  • items, structure is in template) + const suggestionsList = error.suggestions.map((s) => `
  • ${ErrorPageRenderer.escapeHtml(s)}
  • `).join('\n'); + + // Format stderr lines with proper escaping (just the text content) + const stderrOutput = error.stderrLines.map((line) => ErrorPageRenderer.escapeHtml(line)).join('\n'); + + // Use default proxy port for emergency commands + const proxyPort = '4545'; + + const html = this.template + // Page metadata + .replace(/\{\{PAGE_TITLE\}\}/g, 'Dev Server Error') + .replace(/\{\{META_REFRESH\}\}/g, '') + + // Header + .replace(/\{\{ERROR_TITLE\}\}/g, ErrorPageRenderer.escapeHtml(error.title)) + .replace(/\{\{STATUS_CLASS\}\}/g, 'error') + .replace(/\{\{ERROR_STATUS\}\}/g, 'Error Detected') + + // Message content + .replace(/\{\{ERROR_MESSAGE_TEXT\}\}/g, ErrorPageRenderer.escapeHtml(error.message)) + + // Dev server error data + .replace(/\{\{STDERR_OUTPUT\}\}/g, stderrOutput) + .replace(/\{\{PROXY_PORT\}\}/g, proxyPort) + + // Suggestions + .replace(/\{\{SUGGESTIONS_TITLE\}\}/g, 'How to Fix This') + .replace(/\{\{SUGGESTIONS_LIST\}\}/g, suggestionsList) + + // Section visibility (show dev server error, hide others) + .replace(/\{\{SIMPLE_SECTION_CLASS\}\}/g, 'hidden') + .replace(/\{\{RUNTIME_SECTION_CLASS\}\}/g, 'hidden') + .replace(/\{\{DEV_SERVER_SECTION_CLASS\}\}/g, '') + .replace(/\{\{SUGGESTIONS_SECTION_CLASS\}\}/g, '') + + // Auto-refresh + .replace(/\{\{AUTO_REFRESH_CLASS\}\}/g, '') + .replace( + /\{\{AUTO_REFRESH_TEXT\}\}/g, + 'This page will auto-refresh every 5 seconds until the dev server starts successfully' + ); + + return html; } } diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts deleted file mode 100644 index 6ed9e4c..0000000 --- a/src/utils/Logger.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2025, 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. - */ - -/** - * Logger utility for the webapp dev command - * Provides debug logging and formatted output - */ -export class Logger { - private debugEnabled: boolean; - - public constructor(debug = false) { - this.debugEnabled = debug; - } - - /** - * Log an info message - */ - public info(message: string): void { - this.logMessage(message, 'log'); - } - - /** - * Log a warning message - */ - public warn(message: string): void { - this.logMessage(message, 'warn'); - } - - /** - * Log an error message - */ - public error(message: string): void { - this.logMessage(message, 'error'); - } - - /** - * Log a debug message (only if debug mode is enabled) - */ - public debug(message: string): void { - if (this.debugEnabled) { - this.logMessage(`[DEBUG] ${message}`, 'log'); - } - } - - /** - * Check if debug mode is enabled - */ - public isDebugEnabled(): boolean { - return this.debugEnabled; - } - - /** - * Internal method to log messages (suppresses console warnings) - */ - // eslint-disable-next-line class-methods-use-this - private logMessage(message: string, level: 'log' | 'warn' | 'error'): void { - // eslint-disable-next-line no-console - console[level](message); - } -} diff --git a/test/auth/AuthManager.test.ts b/test/auth/AuthManager.test.ts index 5c93cc7..761f20b 100644 --- a/test/auth/AuthManager.test.ts +++ b/test/auth/AuthManager.test.ts @@ -18,17 +18,13 @@ import { expect } from 'chai'; import { TestContext } from '@salesforce/core/testSetup'; import { Org, Connection, SfError } from '@salesforce/core'; import { AuthManager } from '../../src/auth/AuthManager.js'; -import { Logger } from '../../src/utils/Logger.js'; describe('AuthManager', () => { const $$ = new TestContext(); - let logger: Logger; let mockOrg: Partial; let mockConnection: Partial; beforeEach(() => { - logger = new Logger(false); - // Create mock connection mockConnection = { accessToken: 'test-access-token', @@ -52,7 +48,7 @@ describe('AuthManager', () => { it('should successfully initialize with valid org', async () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrg as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); expect(authManager.getInstanceUrl()).to.equal('https://test.salesforce.com'); @@ -61,7 +57,7 @@ describe('AuthManager', () => { it('should throw OrgNotFoundError when org does not exist', async () => { $$.SANDBOX.stub(Org, 'create').rejects(new Error('No authorization information found')); - const authManager = new AuthManager('nonexistent', logger); + const authManager = new AuthManager('nonexistent'); try { await authManager.initialize(); @@ -77,7 +73,7 @@ describe('AuthManager', () => { it('should throw OrgAuthFailedError on other auth errors', async () => { $$.SANDBOX.stub(Org, 'create').rejects(new Error('Authentication failed')); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); try { await authManager.initialize(); @@ -94,7 +90,7 @@ describe('AuthManager', () => { it('should return authorization headers with bearer token', async () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrg as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); const headers = authManager.getAuthHeaders(); @@ -105,7 +101,7 @@ describe('AuthManager', () => { }); it('should throw error if not initialized', () => { - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); try { authManager.getAuthHeaders(); @@ -128,7 +124,7 @@ describe('AuthManager', () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrgWithoutToken as unknown as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); try { @@ -146,7 +142,7 @@ describe('AuthManager', () => { it('should return the Salesforce instance URL', async () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrg as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); const instanceUrl = authManager.getInstanceUrl(); @@ -155,7 +151,7 @@ describe('AuthManager', () => { }); it('should throw error if not initialized', () => { - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); try { authManager.getInstanceUrl(); @@ -171,7 +167,7 @@ describe('AuthManager', () => { it('should refresh token successfully', async () => { const orgCreateStub = $$.SANDBOX.stub(Org, 'create').resolves(mockOrg as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); await authManager.refreshToken(); @@ -185,7 +181,7 @@ describe('AuthManager', () => { orgCreateStub.onFirstCall().resolves(mockOrg as Org); orgCreateStub.onSecondCall().rejects(new Error('Refresh failed')); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); try { @@ -202,7 +198,7 @@ describe('AuthManager', () => { it('should return true for valid token', async () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrg as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); const isValid = await authManager.isTokenValid(); @@ -222,7 +218,7 @@ describe('AuthManager', () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrgWithError as unknown as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); const isValid = await authManager.isTokenValid(); @@ -231,7 +227,7 @@ describe('AuthManager', () => { }); it('should return false if not initialized', async () => { - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); const isValid = await authManager.isTokenValid(); @@ -241,7 +237,7 @@ describe('AuthManager', () => { describe('getOrgAlias', () => { it('should return the target org alias', () => { - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); expect(authManager.getOrgAlias()).to.equal('testorg'); }); @@ -251,14 +247,14 @@ describe('AuthManager', () => { it('should return the org username', async () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrg as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); expect(authManager.getUsername()).to.equal('test@example.com'); }); it('should return undefined if not initialized', () => { - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); expect(authManager.getUsername()).to.be.undefined; }); @@ -268,14 +264,14 @@ describe('AuthManager', () => { it('should return the org ID', async () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrg as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); expect(authManager.getOrgId()).to.equal('00D000000000001'); }); it('should return undefined if not initialized', () => { - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); expect(authManager.getOrgId()).to.be.undefined; }); @@ -285,7 +281,7 @@ describe('AuthManager', () => { it('should attempt token refresh on auth error', async () => { $$.SANDBOX.stub(Org, 'create').resolves(mockOrg as Org); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); const authError = new Error('401 Unauthorized'); @@ -295,7 +291,7 @@ describe('AuthManager', () => { }); it('should return false for non-auth errors', async () => { - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); const networkError = new Error('Network timeout'); @@ -309,7 +305,7 @@ describe('AuthManager', () => { orgCreateStub.onFirstCall().resolves(mockOrg as Org); orgCreateStub.onSecondCall().rejects(new Error('Refresh failed')); - const authManager = new AuthManager('testorg', logger); + const authManager = new AuthManager('testorg'); await authManager.initialize(); const authError = new Error('Token expired'); diff --git a/test/commands/webapp/dev.test.ts b/test/commands/webapp/dev.test.ts index 6ecfcc7..4db4ec8 100644 --- a/test/commands/webapp/dev.test.ts +++ b/test/commands/webapp/dev.test.ts @@ -17,7 +17,6 @@ import { expect } from 'chai'; import { TestContext } from '@salesforce/core/testSetup'; import { SfError } from '@salesforce/core'; -import { Logger } from '../../../src/utils/Logger.js'; import { ErrorHandler } from '../../../src/error/ErrorHandler.js'; import type { WebAppManifest, WebAppDevResult } from '../../../src/config/types.js'; @@ -130,27 +129,6 @@ describe('webapp:dev command integration', () => { }); }); - describe('Logger Integration', () => { - it('should create logger with debug disabled by default', () => { - const logger = new Logger(false); - expect(logger.isDebugEnabled()).to.be.false; - }); - - it('should create logger with debug enabled when specified', () => { - const logger = new Logger(true); - expect(logger.isDebugEnabled()).to.be.true; - }); - - it('should have all required logging methods', () => { - const logger = new Logger(false); - expect(typeof logger.info).to.equal('function'); - expect(typeof logger.error).to.equal('function'); - expect(typeof logger.warn).to.equal('function'); - expect(typeof logger.debug).to.equal('function'); - expect(typeof logger.isDebugEnabled).to.equal('function'); - }); - }); - describe('Configuration Validation', () => { it('should validate manifest with dev.url', () => { const manifest: WebAppManifest = { diff --git a/test/error/ErrorHandler.test.ts b/test/error/ErrorHandler.test.ts index 8e6dde1..ea9eb55 100644 --- a/test/error/ErrorHandler.test.ts +++ b/test/error/ErrorHandler.test.ts @@ -406,146 +406,4 @@ describe('ErrorHandler', () => { expect(actionsString).to.include('sf org login web'); }); }); - - describe('Runtime Error Methods', () => { - it('should create runtime error', () => { - const error = ErrorHandler.createRuntimeError('Something went wrong', 'file.ts:10:5'); - - expect(error).to.be.instanceOf(SfError); - expect(error.message).to.include('Runtime error occurred'); - expect(error.message).to.include('file.ts:10:5'); - expect(error.message).to.include('Something went wrong'); - expect(error.name).to.equal('RuntimeError'); - expect(error.actions).to.have.lengthOf.at.least(1); - }); - - it('should create uncaught exception error', () => { - const originalError = new TypeError('Cannot read property of undefined'); - const error = ErrorHandler.createUncaughtExceptionError(originalError); - - expect(error).to.be.instanceOf(SfError); - expect(error.message).to.include('Uncaught exception'); - expect(error.message).to.include('TypeError'); - expect(error.message).to.include('Cannot read property'); - expect(error.name).to.equal('UncaughtException'); - expect(error.actions).to.have.lengthOf.at.least(1); - }); - - it('should create unhandled rejection error', () => { - const error = ErrorHandler.createUnhandledRejectionError('Promise rejected'); - - expect(error).to.be.instanceOf(SfError); - expect(error.message).to.include('Unhandled promise rejection'); - expect(error.message).to.include('Promise rejected'); - expect(error.name).to.equal('UnhandledRejection'); - expect(error.actions).to.have.lengthOf.at.least(1); - }); - - it('should create unhandled rejection from Error object', () => { - const originalError = new Error('Async operation failed'); - const error = ErrorHandler.createUnhandledRejectionError(originalError); - - expect(error).to.be.instanceOf(SfError); - expect(error.message).to.include('Async operation failed'); - }); - }); - - describe('Context-Aware Suggestions', () => { - it('should provide suggestions for connection refused errors', () => { - const error = new Error('Connection refused') as NodeJS.ErrnoException; - error.code = 'ECONNREFUSED'; - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.length).to.be.greaterThan(0); - expect(suggestions.some((s) => s.toLowerCase().includes('server'))).to.be.true; - }); - - it('should provide suggestions for module not found errors', () => { - const error = new Error('Cannot find module') as Error & { code: string }; - error.name = 'MODULE_NOT_FOUND'; - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.some((s) => s.toLowerCase().includes('install'))).to.be.true; - }); - - it('should provide suggestions for syntax errors', () => { - const error = new SyntaxError('Unexpected token'); - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.some((s) => s.toLowerCase().includes('syntax'))).to.be.true; - }); - - it('should provide suggestions for type errors', () => { - const error = new TypeError('Cannot read property of undefined'); - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.some((s) => s.toLowerCase().includes('type'))).to.be.true; - }); - - it('should provide suggestions for reference errors', () => { - const error = new ReferenceError('Variable is not defined'); - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.some((s) => s.toLowerCase().includes('variable'))).to.be.true; - }); - - it('should provide suggestions for auth errors', () => { - const error = new Error('Authentication failed'); - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.some((s) => s.toLowerCase().includes('auth'))).to.be.true; - }); - - it('should provide suggestions for file not found errors', () => { - const error = new Error('File not found') as NodeJS.ErrnoException; - error.code = 'ENOENT'; - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.some((s) => s.toLowerCase().includes('file'))).to.be.true; - }); - - it('should provide suggestions for permission errors', () => { - const error = new Error('Permission denied') as NodeJS.ErrnoException; - error.code = 'EACCES'; - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.some((s) => s.toLowerCase().includes('permission'))).to.be.true; - }); - - it('should provide suggestions for port in use errors', () => { - const error = new Error('Address already in use') as NodeJS.ErrnoException; - error.code = 'EADDRINUSE'; - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.some((s) => s.toLowerCase().includes('port'))).to.be.true; - }); - - it('should provide generic suggestions for unknown errors', () => { - const error = new Error('Some unknown error'); - - const suggestions = ErrorHandler.getSuggestionsForError(error); - - expect(suggestions).to.be.an('array'); - expect(suggestions.length).to.be.greaterThan(0); - expect(suggestions.some((s) => s.toLowerCase().includes('error message'))).to.be.true; - }); - }); }); diff --git a/test/error/GlobalErrorCapture.test.ts b/test/error/GlobalErrorCapture.test.ts deleted file mode 100644 index cfccc1a..0000000 --- a/test/error/GlobalErrorCapture.test.ts +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright 2025, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { GlobalErrorCapture } from '../../src/error/GlobalErrorCapture.js'; -import type { ErrorMetadata } from '../../src/error/types.js'; - -describe('GlobalErrorCapture', () => { - // Reset singleton after each test - afterEach(() => { - GlobalErrorCapture.resetInstance(); - }); - - describe('Singleton Pattern', () => { - it('should return the same instance', () => { - const instance1 = GlobalErrorCapture.getInstance(); - const instance2 = GlobalErrorCapture.getInstance(); - - expect(instance1).to.equal(instance2); - }); - - it('should reset instance', () => { - const instance1 = GlobalErrorCapture.getInstance(); - GlobalErrorCapture.resetInstance(); - const instance2 = GlobalErrorCapture.getInstance(); - - expect(instance1).to.not.equal(instance2); - }); - }); - - describe('Start and Stop', () => { - it('should start capturing errors', () => { - const capture = GlobalErrorCapture.getInstance(); - capture.start(); - - const stats = capture.getStats(); - expect(stats.isStarted).to.be.true; - expect(stats.captureExceptions).to.be.true; - expect(stats.captureRejections).to.be.true; - }); - - it('should stop capturing errors', () => { - const capture = GlobalErrorCapture.getInstance(); - capture.start(); - capture.stop(); - - const stats = capture.getStats(); - expect(stats.isStarted).to.be.false; - }); - - it('should not start twice', () => { - const capture = GlobalErrorCapture.getInstance(); - capture.start(); - capture.start(); // Should log warning but not throw - - const stats = capture.getStats(); - expect(stats.isStarted).to.be.true; - }); - - it('should handle stop when not started', () => { - const capture = GlobalErrorCapture.getInstance(); - capture.stop(); // Should not throw - - const stats = capture.getStats(); - expect(stats.isStarted).to.be.false; - }); - }); - - describe('Error Capture', () => { - it('should capture error metadata', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new Error('Test error'); - - const metadata = capture.captureError(error, 'test context'); - - expect(metadata.type).to.equal('Error'); - expect(metadata.message).to.equal('Test error'); - expect(metadata.context).to.equal('test context'); - expect(metadata.severity).to.equal('error'); - expect(metadata.stack).to.be.a('string'); - expect(metadata.formattedStack).to.be.an('object'); - expect(metadata.timestamp).to.be.a('string'); - expect(metadata.nodeVersion).to.equal(process.version); - expect(metadata.platform).to.equal(process.platform); - expect(metadata.pid).to.equal(process.pid); - expect(metadata.memoryUsage).to.be.an('object'); - expect(metadata.isUnhandledRejection).to.be.false; - }); - - it('should capture error with code', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new Error('Test error') as NodeJS.ErrnoException; - error.code = 'ENOENT'; - - const metadata = capture.captureError(error); - - expect(metadata.code).to.equal('ENOENT'); - }); - - it('should store last error', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new Error('Test error'); - - expect(capture.getLastError()).to.be.null; - - capture.captureError(error); - // Note: captureError doesn't emit events or store, so last error won't be set - // We need to test with actual uncaught exceptions - }); - - it('should clear last error', () => { - const capture = GlobalErrorCapture.getInstance(); - capture.clearLastError(); - - expect(capture.getLastError()).to.be.null; - }); - }); - - describe('Error Metadata', () => { - it('should include formatted stack trace', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new Error('Test error'); - - const metadata = capture.captureError(error); - - expect(metadata.formattedStack).to.have.property('html'); - expect(metadata.formattedStack).to.have.property('text'); - expect(metadata.formattedStack).to.have.property('frames'); - expect(metadata.formattedStack).to.have.property('filteredCount'); - }); - - it('should include memory usage', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new Error('Test error'); - - const metadata = capture.captureError(error); - - expect(metadata.memoryUsage.heapUsedMB).to.be.a('number'); - expect(metadata.memoryUsage.heapTotalMB).to.be.a('number'); - expect(metadata.memoryUsage.rssMB).to.be.a('number'); - expect(metadata.memoryUsage.externalMB).to.be.a('number'); - expect(metadata.memoryUsage.heapUsedMB).to.be.greaterThan(0); - }); - - it('should include original error', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new Error('Test error'); - - const metadata = capture.captureError(error); - - expect(metadata.originalError).to.equal(error); - }); - }); - - describe('Configuration Options', () => { - it('should respect custom onError callback', () => { - const capture = GlobalErrorCapture.getInstance({ - onError: (metadata) => { - // Callback exists and would be called on actual errors - expect(metadata).to.be.an('object'); - }, - }); - - // Verify the callback was set - expect(capture).to.be.an.instanceof(GlobalErrorCapture); - }); - - it('should respect filterNodeModules option', () => { - const captureFiltered = GlobalErrorCapture.getInstance({ - filterNodeModules: true, - }); - - const stats = captureFiltered.getStats(); - expect(stats).to.be.an('object'); - }); - - it('should respect workspaceRoot option', () => { - const capture = GlobalErrorCapture.getInstance({ - workspaceRoot: '/custom/workspace', - }); - - const error = new Error('Test error'); - const metadata = capture.captureError(error); - - expect(metadata).to.be.an('object'); - }); - }); - - describe('Statistics', () => { - it('should provide capture statistics', () => { - const capture = GlobalErrorCapture.getInstance(); - - const stats = capture.getStats(); - - expect(stats).to.have.property('isStarted'); - expect(stats).to.have.property('hasLastError'); - expect(stats).to.have.property('captureExceptions'); - expect(stats).to.have.property('captureRejections'); - expect(stats.isStarted).to.be.false; - expect(stats.hasLastError).to.be.false; - }); - - it('should update statistics when started', () => { - const capture = GlobalErrorCapture.getInstance(); - capture.start(); - - const stats = capture.getStats(); - - expect(stats.isStarted).to.be.true; - }); - }); - - describe('Error Types', () => { - it('should handle TypeError', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new TypeError('Cannot read property of undefined'); - - const metadata = capture.captureError(error); - - expect(metadata.type).to.equal('TypeError'); - expect(metadata.message).to.include('Cannot read property'); - }); - - it('should handle ReferenceError', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new ReferenceError('Variable is not defined'); - - const metadata = capture.captureError(error); - - expect(metadata.type).to.equal('ReferenceError'); - expect(metadata.message).to.include('not defined'); - }); - - it('should handle SyntaxError', () => { - const capture = GlobalErrorCapture.getInstance(); - const error = new SyntaxError('Unexpected token'); - - const metadata = capture.captureError(error); - - expect(metadata.type).to.equal('SyntaxError'); - expect(metadata.message).to.include('Unexpected token'); - }); - - it('should handle non-Error objects', () => { - const capture = GlobalErrorCapture.getInstance(); - const metadata = capture.captureError('String error'); - - expect(metadata.message).to.equal('String error'); - expect(metadata.type).to.equal('Error'); - }); - }); - - describe('Event Emission', () => { - it('should emit error event', (done) => { - const capture = GlobalErrorCapture.getInstance(); - capture.start(); - - capture.on('error', (metadata: ErrorMetadata) => { - expect(metadata).to.be.an('object'); - expect(metadata.message).to.equal('Test error'); - done(); - }); - - // Note: In actual usage, this would be triggered by uncaught exceptions - // For testing, we can't easily simulate process-level events - // So this test verifies the event emitter is set up correctly - done(); - }); - }); - - describe('Intentional Exit Filtering', () => { - it('should ignore oclif EEXIT errors', () => { - type ExitError = Error & { code: string; oclif: { exit: number } }; - const exitError = new Error('EEXIT: 130') as ExitError; - exitError.code = 'EEXIT'; - exitError.oclif = { exit: 130 }; - - // Now isIntentionalExit is a static method we can test directly - const isIntentional = ( - GlobalErrorCapture as unknown as { isIntentionalExit: (error: Error) => boolean } - ).isIntentionalExit(exitError); - - expect(isIntentional).to.be.true; - }); - - it('should ignore errors with skipOclifErrorHandling flag', () => { - type SkipError = Error & { skipOclifErrorHandling: boolean }; - const exitError = new Error('Exit signal') as SkipError; - exitError.skipOclifErrorHandling = true; - - const isIntentional = ( - GlobalErrorCapture as unknown as { isIntentionalExit: (error: Error) => boolean } - ).isIntentionalExit(exitError); - - expect(isIntentional).to.be.true; - }); - - it('should ignore SIGINT/SIGTERM errors', () => { - const sigintError = new Error('Process terminated with SIGINT'); - - const isIntentional = ( - GlobalErrorCapture as unknown as { isIntentionalExit: (error: Error) => boolean } - ).isIntentionalExit(sigintError); - - expect(isIntentional).to.be.true; - }); - - it('should NOT ignore regular errors', () => { - const regularError = new Error('This is a real error'); - - const isIntentional = ( - GlobalErrorCapture as unknown as { isIntentionalExit: (error: Error) => boolean } - ).isIntentionalExit(regularError); - - expect(isIntentional).to.be.false; - }); - - it('should NOT ignore TypeError with different message', () => { - const typeError = new TypeError('Cannot read property of undefined'); - - const isIntentional = ( - GlobalErrorCapture as unknown as { isIntentionalExit: (error: Error) => boolean } - ).isIntentionalExit(typeError); - - expect(isIntentional).to.be.false; - }); - }); -}); diff --git a/test/error/StackTraceFormatter.test.ts b/test/error/StackTraceFormatter.test.ts deleted file mode 100644 index 558ead7..0000000 --- a/test/error/StackTraceFormatter.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright 2025, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { StackTraceFormatter } from '../../src/error/StackTraceFormatter.js'; - -describe('StackTraceFormatter', () => { - describe('Stack Trace Parsing', () => { - it('should parse a standard V8 stack trace', () => { - const stackTrace = `Error: Test error - at testFunction (/path/to/file.ts:10:5) - at Object. (/path/to/another.ts:20:10) - at Module._compile (node:internal/modules/cjs/loader:1126:14)`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(2); // node:internal filtered - expect(result.frames[0].functionName).to.equal('testFunction'); - expect(result.frames[0].fileName).to.equal('/path/to/file.ts'); - expect(result.frames[0].lineNumber).to.equal(10); - expect(result.frames[0].columnNumber).to.equal(5); - }); - - it('should parse anonymous functions', () => { - const stackTrace = `Error: Test error - at /path/to/file.ts:15:20`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(1); - expect(result.frames[0].functionName).to.equal('anonymous'); - }); - - it('should parse async stack frames', () => { - const stackTrace = `Error: Test error - at async myAsyncFunction (/path/to/file.ts:30:5)`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(1); - expect(result.frames[0].functionName).to.equal('myAsyncFunction'); - }); - }); - - describe('Filtering', () => { - it('should filter node_modules by default', () => { - const stackTrace = `Error: Test error - at myFunction (/path/to/file.ts:10:5) - at someLib (/path/node_modules/some-lib/index.js:100:20) - at anotherFunction (/path/to/another.ts:20:10)`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(2); - expect(result.filteredCount).to.equal(1); - expect(result.frames.every((f) => !f.fileName.includes('node_modules'))).to.be.true; - }); - - it('should filter Node.js internals by default', () => { - const stackTrace = `Error: Test error - at myFunction (/path/to/file.ts:10:5) - at Module._compile (node:internal/modules/cjs/loader:1126:14) - at internal/timers (internal/timers:123:5)`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(1); - expect(result.filteredCount).to.equal(2); - expect(result.frames[0].fileName).to.equal('/path/to/file.ts'); - }); - - it('should not filter when disabled', () => { - const stackTrace = `Error: Test error - at myFunction (/path/to/file.ts:10:5) - at someLib (/path/node_modules/some-lib/index.js:100:20)`; - - const formatter = new StackTraceFormatter({ - filterNodeModules: false, - filterNodeInternals: false, - }); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(2); - expect(result.filteredCount).to.equal(0); - }); - }); - - describe('Formatting', () => { - it('should generate HTML formatted output', () => { - const stackTrace = `Error: Test error - at testFunction (/path/to/file.ts:10:5)`; - - const formatter = new StackTraceFormatter({ enableHtmlFormatting: true }); - const result = formatter.format(stackTrace); - - expect(result.html).to.include('stack-frame'); - expect(result.html).to.include('frame-function'); - expect(result.html).to.include('testFunction'); - expect(result.html).to.include('frame-file'); - expect(result.html).to.include('frame-line'); - }); - - it('should generate plain text formatted output', () => { - const stackTrace = `Error: Test error - at testFunction (/path/to/file.ts:10:5) - at anotherFunction (/path/to/another.ts:20:10)`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.text).to.include('1. testFunction'); - expect(result.text).to.include('2. anotherFunction'); - expect(result.text).to.include('/path/to/file.ts:10:5'); - expect(result.text).to.include('/path/to/another.ts:20:10'); - }); - - it('should handle relative paths when workspace root provided', () => { - const stackTrace = `Error: Test error - at testFunction (/workspace/src/file.ts:10:5)`; - - const formatter = new StackTraceFormatter({ workspaceRoot: '/workspace' }); - const result = formatter.format(stackTrace); - - // Normalize path separators for cross-platform compatibility (Windows uses backslashes) - const normalizedText = result.text.replace(/\\/g, '/'); - expect(normalizedText).to.include('src/file.ts'); - expect(normalizedText).to.not.include('/workspace/src/file.ts'); - }); - - it('should truncate long absolute paths', () => { - const longPath = '/very/long/path/that/is/too/long/to/display/nicely/in/stack/traces/file.ts'; - const stackTrace = `Error: Test error - at testFunction (${longPath}:10:5)`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - // Path should either be truncated with ... or displayed as-is if under limit - expect(result.text).to.be.a('string'); - expect(result.text).to.include('testFunction'); - }); - }); - - describe('Static Methods', () => { - it('should format error directly', () => { - const error = new Error('Test error'); - const result = StackTraceFormatter.formatError(error); - - expect(result.frames.length).to.be.greaterThan(0); - expect(result.html).to.be.a('string'); - expect(result.text).to.be.a('string'); - }); - - it('should extract error location', () => { - const error = new Error('Test error'); - const location = StackTraceFormatter.extractErrorLocation(error); - - expect(location).to.be.a('string'); - expect(location).to.match(/:\d+:\d+$/); // Should end with :line:column - }); - - it('should return null for error without stack', () => { - const error = new Error('Test error'); - delete error.stack; - const location = StackTraceFormatter.extractErrorLocation(error); - - expect(location).to.be.null; - }); - }); - - describe('Edge Cases', () => { - it('should handle empty stack trace', () => { - const stackTrace = 'Error: Test error'; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(0); - expect(result.html).to.include('No stack frames available'); - expect(result.text).to.include('No stack frames available'); - }); - - it('should handle malformed stack lines', () => { - const stackTrace = `Error: Test error - at testFunction (/path/to/file.ts:10:5) - some malformed line - at anotherFunction (/path/to/another.ts:20:10)`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(2); - }); - - it('should respect maxFrames limit', () => { - const stackTrace = `Error: Test error - at function1 (/path/file1.ts:10:5) - at function2 (/path/file2.ts:20:5) - at function3 (/path/file3.ts:30:5) - at function4 (/path/file4.ts:40:5) - at function5 (/path/file5.ts:50:5)`; - - const formatter = new StackTraceFormatter({ maxFrames: 3 }); - const result = formatter.format(stackTrace); - - expect(result.frames).to.have.lengthOf(3); - expect(result.filteredCount).to.equal(2); - }); - - it('should escape HTML in formatted output', () => { - const stackTrace = `Error: Test error - at (/path/to/file.ts:10:5)`; - - const formatter = new StackTraceFormatter(); - const result = formatter.format(stackTrace); - - expect(result.html).to.not.include('', - errorMessage: '', - formattedStackHtml: '
    safe html
    ', - formattedStackText: 'stack text', - timestamp: '2025-01-01T00:00:00.000Z', - timestampFormatted: 'January 1, 2025', - severity: 'error', - metadata: { - nodeVersion: 'v18.0.0', - platform: 'darwin', - pid: 12_345, - heapUsedMB: 50, - heapTotalMB: 100, - rssMB: 150, - }, - suggestions: [], - errorReportJson: '{}', - }; - - const html = renderer.renderRuntimeError(data); - - expect(html).to.not.include('