Skip to content

Commit 27d2684

Browse files
author
Ankit Singh
committed
W-19978121:add runtimeEnv util and reusable ErrorDisplay LWC; show env in hmrTest
1 parent a069475 commit 27d2684

File tree

10 files changed

+377
-4
lines changed

10 files changed

+377
-4
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
.title {
2+
font-weight: 700;
3+
font-size: 14px;
4+
color: #c23934;
5+
margin-bottom: 8px;
6+
}
7+
.kv { margin-bottom: 12px; }
8+
.kv-row { display: flex; gap: 8px; margin: 2px 0; font-size: 12px; }
9+
.kv-label { width: 90px; color: #6b6d70; font-weight: 600; }
10+
.kv-value { color: #080707; }
11+
.meta {
12+
display: flex;
13+
gap: 6px;
14+
margin-bottom: 12px;
15+
}
16+
.pill {
17+
background: #f3f2f2;
18+
border-radius: 10px;
19+
padding: 2px 8px;
20+
font-size: 12px;
21+
}
22+
.stack {
23+
background: #fffbe6;
24+
border: 1px solid #ece0a1;
25+
padding: 8px;
26+
margin-bottom: 12px;
27+
max-height: 180px;
28+
overflow: auto;
29+
}
30+
.stack.empty {
31+
color: #6b6d70;
32+
background: #fafafa;
33+
border: 1px dashed #d8dde6;
34+
}
35+
.section-title {
36+
font-weight: 600;
37+
font-size: 12px;
38+
color: #3e3e3c;
39+
margin: 6px 0 4px;
40+
}
41+
.stack-line {
42+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
43+
font-size: 12px;
44+
white-space: pre-wrap;
45+
}
46+
.props-title {
47+
font-weight: 600;
48+
margin-bottom: 4px;
49+
}
50+
.prop-row {
51+
display: flex;
52+
gap: 8px;
53+
font-size: 12px;
54+
}
55+
.k { color: #6b6d70; }
56+
.v { color: #080707; }
57+
.actions { margin-top: 12px; }
58+
59+
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<template>
2+
<lightning-card title="Preview Error" icon-name="utility:warning">
3+
<div class="slds-p-around_medium">
4+
<div class="title">{view.title}</div>
5+
<div class="section-title">Details</div>
6+
<div class="kv">
7+
<div class="kv-row">
8+
<span class="kv-label">Message</span>
9+
<span class="kv-value">{view.title}</span>
10+
</div>
11+
<div class="kv-row">
12+
<span class="kv-label">Error ID</span>
13+
<span class="kv-value">{view.errorId}</span>
14+
</div>
15+
<div class="kv-row">
16+
<span class="kv-label">Component</span>
17+
<span class="kv-value">{view.componentName}</span>
18+
</div>
19+
<div class="kv-row">
20+
<span class="kv-label">Timestamp</span>
21+
<span class="kv-value">{view.timestamp}</span>
22+
</div>
23+
</div>
24+
<div class="section-title">Stack Trace</div>
25+
<template if:true={view.stackLines}>
26+
<div class="stack">
27+
<template for:each={view.stackLines} for:item="line">
28+
<div key={line} class="stack-line">{line}</div>
29+
</template>
30+
</div>
31+
</template>
32+
<template if:false={view.stackLines}>
33+
<div class="stack empty">No stack trace available.</div>
34+
</template>
35+
<template if:true={hasProps}>
36+
<div class="props">
37+
<div class="props-title">Props</div>
38+
<template for:each={propEntries} for:item="entry">
39+
<div key={entry.key} class="prop-row">
40+
<span class="k">{entry.key}</span>
41+
<span class="v">{entry.value}</span>
42+
</div>
43+
</template>
44+
</div>
45+
</template>
46+
<div class="actions">
47+
<lightning-button variant="brand" label="Copy Details" onclick={handleCopy}></lightning-button>
48+
</div>
49+
</div>
50+
</lightning-card>
51+
</template>
52+
53+
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { LightningElement, api, track } from 'lwc';
2+
3+
export default class ErrorDisplay extends LightningElement {
4+
_payload;
5+
@track propEntriesInternal = [];
6+
@track view = { title: 'No error payload provided.', errorId: 'LWC-Runtime-EID-DEMO', timestamp: '', componentName: 'c-unknown', stackLines: [] };
7+
8+
@api
9+
get payload() {
10+
return this._payload;
11+
}
12+
set payload(value) {
13+
this._payload = value;
14+
this.computeView();
15+
this.normalizeProps();
16+
}
17+
18+
connectedCallback() {
19+
// Provide a visible default so demos render even if parent forgets to pass a payload
20+
if (!this._payload) {
21+
// Default to the new wrapper format with one error sample
22+
this._payload = {
23+
success: true,
24+
count: 1,
25+
errors: [
26+
{
27+
errorId: 'a1b2c3d4-e5f6-4a1b-8c9d-0e1f2a3b4c5d',
28+
timestamp: '2025-10-21T10:30:45.123Z',
29+
error: {
30+
name: 'ReferenceError',
31+
message: 'nonExistentMethod is not defined',
32+
stack: 'ReferenceError: nonExistentMethod is not defined\n at ErrorTestComponent.connectedCallback...',
33+
sanitizedStack: [
34+
{
35+
functionName: 'connectedCallback',
36+
fileName: 'errorTestComponent.js',
37+
lineNumber: 5,
38+
columnNumber: 10,
39+
isLocalSource: true,
40+
},
41+
],
42+
},
43+
component: {
44+
name: 'c-error-test-component',
45+
namespace: 'c',
46+
tagName: 'c-error-test-component',
47+
lifecycle: 'connectedCallback',
48+
filePath: null,
49+
},
50+
runtime: {
51+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...',
52+
viewport: { width: 1920, height: 1080 },
53+
url: 'https://myorg.lightning.force.com/...',
54+
lwcVersion: null,
55+
isDevelopment: true,
56+
},
57+
state: { props: { recordId: '001xx000003DHP0' }, publicProperties: ['recordId', 'objectApiName'], isConnected: true },
58+
source: { fileName: 'errorTestComponent.js', lineNumber: 5, columnNumber: 10 },
59+
metadata: { severity: 'error', wasHandled: false, occurrenceCount: 1, tags: ['runtime', 'browser', 'lwc'] },
60+
},
61+
],
62+
};
63+
}
64+
this.computeView();
65+
this.normalizeProps();
66+
}
67+
68+
computeView() {
69+
try {
70+
let title = 'Unknown error';
71+
let errorId = 'N/A';
72+
let timestamp = '';
73+
let componentName = 'c-unknown';
74+
let stackLines = [];
75+
76+
if (this._payload && Array.isArray(this._payload.errors) && this._payload.errors.length > 0) {
77+
const e = this._payload.errors[0];
78+
title = (e && e.error && (e.error.message || e.error.name)) || title;
79+
errorId = e && e.errorId ? e.errorId : errorId;
80+
timestamp = e && e.timestamp ? e.timestamp : '';
81+
componentName = (e && e.component && (e.component.name || e.component.tagName)) || componentName;
82+
83+
if (e && e.error && Array.isArray(e.error.sanitizedStack) && e.error.sanitizedStack.length) {
84+
stackLines = e.error.sanitizedStack.map((f) => {
85+
const fn = f.functionName || '<anonymous>';
86+
const file = f.fileName || 'unknown';
87+
const line = f.lineNumber != null ? f.lineNumber : '?';
88+
const col = f.columnNumber != null ? f.columnNumber : '?';
89+
return `${fn} (${file}:${line}:${col})`;
90+
});
91+
} else if (e && e.error && e.error.stack) {
92+
stackLines = String(e.error.stack)
93+
.split('\n')
94+
.map((l) => l.trim())
95+
.filter(Boolean)
96+
.slice(0, 30);
97+
}
98+
99+
// also set props for normalizeProps()
100+
this._propsForView = (e && e.state && e.state.props) || {};
101+
} else if (this._payload) {
102+
// Back-compat with older simple shape
103+
title = this._payload.errorMessage || title;
104+
errorId = this._payload.errorId || errorId;
105+
timestamp = this._payload.timestamp || '';
106+
componentName = this._payload.componentName || componentName;
107+
stackLines = Array.isArray(this._payload.stackTrace) ? this._payload.stackTrace : [];
108+
this._propsForView = this._payload.props || {};
109+
}
110+
111+
this.view = { title, errorId, timestamp, componentName, stackLines };
112+
} catch (_) {
113+
// keep default view
114+
}
115+
}
116+
117+
normalizeProps() {
118+
const p = this._propsForView || {};
119+
if (!p) {
120+
this.propEntriesInternal = [];
121+
return;
122+
}
123+
const entries = [];
124+
for (const k of Object.keys(p)) {
125+
let v = p[k];
126+
try {
127+
if (typeof v === 'object') v = JSON.stringify(v);
128+
} catch (e) {
129+
// ignore stringify errors
130+
}
131+
entries.push({ key: k, value: String(v) });
132+
}
133+
// Only assign if changed to avoid unnecessary re-renders
134+
const prev = this.propEntriesInternal || [];
135+
const sameLength = prev.length === entries.length;
136+
const samePairs = sameLength && prev.every((it, i) => it.key === entries[i].key && it.value === entries[i].value);
137+
if (!samePairs) {
138+
this.propEntriesInternal = entries;
139+
}
140+
}
141+
142+
get hasProps() {
143+
return this.propEntriesInternal && this.propEntriesInternal.length > 0;
144+
}
145+
146+
get propEntries() {
147+
return this.propEntriesInternal;
148+
}
149+
150+
async handleCopy() {
151+
const text = JSON.stringify(this._payload || {}, null, 2);
152+
try {
153+
await navigator.clipboard.writeText(text);
154+
} catch (e) {
155+
// fallback
156+
const ta = document.createElement('textarea');
157+
ta.value = text;
158+
ta.style.position = 'fixed';
159+
ta.style.opacity = '0';
160+
document.body.appendChild(ta);
161+
ta.focus();
162+
ta.select();
163+
try {
164+
document.execCommand('copy');
165+
} catch (_) {
166+
// ignore
167+
}
168+
document.body.removeChild(ta);
169+
}
170+
}
171+
}
172+
173+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>66.0</apiVersion>
4+
<isExposed>true</isExposed>
5+
<targets>
6+
<target>lightning__AppPage</target>
7+
<target>lightning__RecordPage</target>
8+
<target>lightning__HomePage</target>
9+
</targets>
10+
</LightningComponentBundle>

test/projects/sfdx-lwc-project/force-app/main/default/lwc/hmrTest/hmrTest.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
.important-text {
22
font-weight: bold;
3-
color: blue;
3+
color: rgb(190, 30, 121);
44
padding-bottom: 10px;
55
}
66

77
.footer-text {
88
font-style: italic;
9-
color: grey;
9+
color: rgb(193, 12, 12);
1010
font-size: 0.8rem;
1111
}
1212

test/projects/sfdx-lwc-project/force-app/main/default/lwc/hmrTest/hmrTest.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
<template>
2-
<lightning-card title="HMR Test Component - Enhanced" icon-name="custom:custom14">
2+
<lightning-card title="HMR Test Component - ok" icon-name="custom:custom14">
33
<div class="slds-m-around_medium">
44
<p class="important-text">Hello from HMR Test!</p>
55
<p>Current Count: {counter}</p>
6-
<lightning-button label="Increment" onclick="{handleIncrement}"></lightning-button>
6+
<lightning-button label="Increment" onclick={handleIncrement}></lightning-button>
77
<br /><br />
88
<lightning-badge label="HMR Active"></lightning-badge>
99
<br /><br />
1010
<lightning-input label="Sample Input" placeholder="Type something..."></lightning-input>
11+
<br /><br />
12+
<div class="slds-text-color_weak">Env → Browser: {env.isBrowser} | VS Code: {env.isVSCode} | Origin: {env.origin}</div>
1113
</div>
1214
<p class="footer-text slds-m-around_medium">Some text to modify for HMR.</p>
1315
</lightning-card>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
import { LightningElement, track } from 'lwc';
2+
import { isVSCodeExtension, isBrowser, getOriginSafe } from 'c/runtimeEnv';
23

34
export default class HmrTest extends LightningElement {
45
@track counter = 0;
6+
@track env = { isVSCode: false, isBrowser: true, origin: '' };
57

68
handleIncrement() {
79
this.counter++;
810
}
11+
12+
connectedCallback() {
13+
// Verify runtimeEnv module import
14+
// eslint-disable-next-line no-console
15+
console.log('[runtimeEnv]', {
16+
isVSCode: isVSCodeExtension(),
17+
isBrowser: isBrowser(),
18+
origin: getOriginSafe(),
19+
});
20+
21+
// Update on-screen env status for quick verification
22+
this.env = {
23+
isVSCode: isVSCodeExtension(),
24+
isBrowser: isBrowser(),
25+
origin: getOriginSafe(),
26+
};
27+
}
928
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
<!-- Renderless utility component -->
3+
</template>
4+
5+

0 commit comments

Comments
 (0)