Skip to content

Commit 26eb2b5

Browse files
refactor: address comments.
1 parent 64f6103 commit 26eb2b5

4 files changed

Lines changed: 109 additions & 44 deletions

File tree

playwright/app.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ const setComponentEditorSource = async (page: Page, source: string) => {
1313
await editorContent.fill(source)
1414
}
1515

16+
const setStylesEditorSource = async (page: Page, source: string) => {
17+
const editorContent = page.locator('.styles-panel .cm-content').first()
18+
await editorContent.fill(source)
19+
}
20+
1621
test('renders default playground preview', async ({ page }) => {
1722
await waitForInitialRender(page)
1823

@@ -169,6 +174,24 @@ test('renders with sass style mode', async ({ page }) => {
169174
await expect(previewItems.first()).toContainText('apple')
170175
})
171176

177+
test('style compilation errors populate styles diagnostics scope', async ({ page }) => {
178+
await waitForInitialRender(page)
179+
180+
await page.locator('#style-mode').selectOption('sass')
181+
await setStylesEditorSource(page, '.card { color: $missing; }')
182+
183+
await expect(page.locator('#status')).toHaveText('Error')
184+
await expect(page.locator('#diagnostics-toggle')).toHaveClass(
185+
/diagnostics-toggle--error/,
186+
)
187+
188+
await page.locator('#diagnostics-toggle').click()
189+
await expect(page.locator('#diagnostics-styles')).toContainText(
190+
'Style compilation failed.',
191+
)
192+
await expect(page.locator('#diagnostics-styles')).toContainText('Undefined variable')
193+
})
194+
172195
test('clear component action opens confirm dialog and can be canceled', async ({
173196
page,
174197
}) => {

src/app.js

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ let lessCompiler = null
4848
let lightningCssWasm = null
4949
let coreRuntime = null
5050
let typeScriptCompiler = null
51+
let typeScriptCompilerProvider = null
5152
let typeScriptLibFiles = null
5253
let compiledStylesCache = {
5354
key: null,
@@ -132,7 +133,7 @@ const initializeCodeEditors = async () => {
132133
jsxHost.remove()
133134
cssHost.remove()
134135
const message = error instanceof Error ? error.message : String(error)
135-
setStatus(`Editor fallback: ${message}`)
136+
setStatus(`Editor fallback: ${message}`, 'neutral')
136137
}
137138
}
138139

@@ -174,25 +175,9 @@ const ensureCoreRuntime = async () => {
174175
}
175176
}
176177

177-
const inferStatusLevel = text => {
178-
if (text === 'Rendering…' || text === 'Loading CDN assets…') {
179-
return 'pending'
180-
}
181-
182-
if (
183-
text === 'Error' ||
184-
text === 'Copy failed' ||
185-
text.startsWith('Rendered (Type errors:')
186-
) {
187-
return 'error'
188-
}
189-
190-
return 'neutral'
191-
}
192-
193-
const setStatus = (text, level = inferStatusLevel(text)) => {
178+
const setStatus = (text, level) => {
194179
statusNode.textContent = text
195-
statusLevel = level
180+
statusLevel = level ?? 'neutral'
196181
updateUiIssueIndicators()
197182
}
198183

@@ -368,6 +353,10 @@ const setTypeDiagnosticsDetails = ({ headline, lines = [], level = 'muted' }) =>
368353
setDiagnosticsScope('component', { headline, lines, level })
369354
}
370355

356+
const setStyleDiagnosticsDetails = ({ headline, lines = [], level = 'muted' }) => {
357+
setDiagnosticsScope('styles', { headline, lines, level })
358+
}
359+
371360
const setTypecheckButtonLoading = isLoading => {
372361
if (!typecheckButton) {
373362
return
@@ -403,12 +392,12 @@ const scheduleTypeRecheck = () => {
403392

404393
const setRenderedStatus = () => {
405394
if (lastTypeErrorCount > 0) {
406-
setStatus(`Rendered (Type errors: ${lastTypeErrorCount})`)
395+
setStatus(`Rendered (Type errors: ${lastTypeErrorCount})`, 'error')
407396
return
408397
}
409398

410399
if (statusNode.textContent.startsWith('Rendered (Type errors:')) {
411-
setStatus('Rendered')
400+
setStatus('Rendered', 'neutral')
412401
}
413402
}
414403

@@ -447,6 +436,7 @@ const ensureTypeScriptCompiler = async () => {
447436
try {
448437
const loaded = await importFromCdnWithFallback(cdnImports.typescript)
449438
typeScriptCompiler = loaded.module.default ?? loaded.module
439+
typeScriptCompilerProvider = loaded.provider ?? null
450440

451441
if (typeof typeScriptCompiler.transpileModule !== 'function') {
452442
throw new Error(`transpileModule export was not found from ${loaded.url}`)
@@ -474,7 +464,11 @@ const normalizeVirtualFileName = fileName =>
474464
typeof fileName === 'string' && fileName.startsWith('/') ? fileName.slice(1) : fileName
475465

476466
const fetchTypeScriptLibText = async fileName => {
477-
const attempts = getTypeScriptLibUrls(fileName).map(async url => {
467+
const urls = getTypeScriptLibUrls(fileName, {
468+
typeScriptProvider: typeScriptCompilerProvider,
469+
})
470+
471+
const attempts = urls.map(async url => {
478472
const response = await fetch(url)
479473
if (!response.ok) {
480474
throw new Error(`HTTP ${response.status} from ${url}`)
@@ -486,7 +480,17 @@ const fetchTypeScriptLibText = async fileName => {
486480
try {
487481
return await Promise.any(attempts)
488482
} catch (error) {
489-
const message = error instanceof Error ? error.message : String(error)
483+
let message = error instanceof Error ? error.message : String(error)
484+
485+
if (error instanceof AggregateError) {
486+
const reasons = Array.from(error.errors ?? [])
487+
.slice(0, 3)
488+
.map(reason => (reason instanceof Error ? reason.message : String(reason)))
489+
const reasonSummary = reasons.length ? ` Causes: ${reasons.join(' | ')}` : ''
490+
491+
message = `Tried URLs: ${urls.join(', ')}.${reasonSummary}`
492+
}
493+
490494
throw new Error(`Unable to fetch TypeScript lib file ${fileName}: ${message}`, {
491495
cause: error,
492496
})
@@ -678,7 +682,7 @@ const runTypeDiagnostics = async runId => {
678682
})
679683

680684
if (statusNode.textContent.startsWith('Rendered (Type errors:')) {
681-
setStatus('Rendered')
685+
setStatus('Rendered', 'neutral')
682686
}
683687
} finally {
684688
activeTypeDiagnosticsRuns = Math.max(0, activeTypeDiagnosticsRuns - 1)
@@ -703,7 +707,7 @@ const markTypeDiagnosticsStale = () => {
703707
})
704708

705709
if (statusNode.textContent.startsWith('Rendered (Type errors:')) {
706-
setStatus('Rendered')
710+
setStatus('Rendered', 'neutral')
707711
}
708712
}
709713

@@ -825,7 +829,7 @@ const clearComponentSource = () => {
825829
lastTypeErrorCount = 0
826830
hasUnresolvedTypeErrors = false
827831
clearTypeRecheckTimer()
828-
setStatus('Component cleared')
832+
setStatus('Component cleared', 'neutral')
829833

830834
if (!jsxCodeEditor) {
831835
maybeRender()
@@ -835,7 +839,7 @@ const clearComponentSource = () => {
835839
const clearStylesSource = () => {
836840
setCssSource('')
837841
clearDiagnosticsScope('styles')
838-
setStatus('Styles cleared')
842+
setStatus('Styles cleared', 'neutral')
839843
if (!cssCodeEditor) {
840844
maybeRender()
841845
}
@@ -885,18 +889,18 @@ const copyTextToClipboard = async text => {
885889
const copyComponentSource = async () => {
886890
try {
887891
await copyTextToClipboard(getJsxSource())
888-
setStatus('Component copied')
892+
setStatus('Component copied', 'neutral')
889893
} catch {
890-
setStatus('Copy failed')
894+
setStatus('Copy failed', 'error')
891895
}
892896
}
893897

894898
const copyStylesSource = async () => {
895899
try {
896900
await copyTextToClipboard(getCssSource())
897-
setStatus('Styles copied')
901+
setStatus('Styles copied', 'neutral')
898902
} catch {
899-
setStatus('Copy failed')
903+
setStatus('Copy failed', 'error')
900904
}
901905
}
902906

@@ -1304,6 +1308,7 @@ const compileStyles = async () => {
13041308
setStyleCompiling(shouldShowSpinner)
13051309

13061310
if (!shouldShowSpinner) {
1311+
clearDiagnosticsScope('styles')
13071312
const output = { css: cssSource, moduleExports: null }
13081313
compiledStylesCache = {
13091314
key: cacheKey,
@@ -1346,11 +1351,25 @@ const compileStyles = async () => {
13461351
css: compiledCss,
13471352
moduleExports,
13481353
}
1354+
clearDiagnosticsScope('styles')
13491355
compiledStylesCache = {
13501356
key: cacheKey,
13511357
value: output,
13521358
}
13531359
return output
1360+
} catch (error) {
1361+
const message = error instanceof Error ? error.message : String(error)
1362+
const lines = message
1363+
.split('\n')
1364+
.map(line => line.trimEnd())
1365+
.filter(line => line.trim().length > 0)
1366+
1367+
setStyleDiagnosticsDetails({
1368+
headline: 'Style compilation failed.',
1369+
lines,
1370+
level: 'error',
1371+
})
1372+
throw error
13541373
} finally {
13551374
setStyleCompiling(false)
13561375
}
@@ -1493,18 +1512,18 @@ const renderReact = async () => {
14931512
const renderPreview = async () => {
14941513
scheduled = null
14951514
updateStyleWarning()
1496-
setStatus(hasCompletedInitialRender ? 'Rendering…' : 'Loading CDN assets…')
1515+
setStatus(hasCompletedInitialRender ? 'Rendering…' : 'Loading CDN assets…', 'pending')
14971516

14981517
try {
14991518
if (renderMode.value === 'react') {
15001519
await renderReact()
15011520
} else {
15021521
await renderDom()
15031522
}
1504-
setStatus('Rendered')
1523+
setStatus('Rendered', 'neutral')
15051524
setRenderedStatus()
15061525
} catch (error) {
1507-
setStatus('Error')
1526+
setStatus('Error', 'error')
15081527
const target = getRenderTarget()
15091528
clearTarget(target)
15101529
const message = document.createElement('pre')
@@ -1560,7 +1579,7 @@ if (diagnosticsClearComponent) {
15601579
hasUnresolvedTypeErrors = false
15611580
clearTypeRecheckTimer()
15621581
if (statusNode.textContent.startsWith('Rendered (Type errors:')) {
1563-
setStatus('Rendered')
1582+
setStatus('Rendered', 'neutral')
15641583
}
15651584
})
15661585
}
@@ -1571,7 +1590,7 @@ if (diagnosticsClearAll) {
15711590
hasUnresolvedTypeErrors = false
15721591
clearTypeRecheckTimer()
15731592
if (statusNode.textContent.startsWith('Rendered (Type errors:')) {
1574-
setStatus('Rendered')
1593+
setStatus('Rendered', 'neutral')
15751594
}
15761595
})
15771596
}

src/cdn.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ const importFromCdnCandidateAt = async (importCandidates, index, firstError = nu
218218

219219
try {
220220
const module = await import(url)
221-
return { module, url }
221+
return { module, url, provider: importCandidates[index].provider }
222222
} catch (error) {
223223
return importFromCdnCandidateAt(importCandidates, index + 1, firstError ?? error)
224224
}
@@ -231,18 +231,36 @@ const typeScriptVersion = '5.9.3'
231231
const typeScriptLibBaseByProvider = {
232232
esm: `https://esm.sh/typescript@${typeScriptVersion}/lib`,
233233
unpkg: `https://unpkg.com/typescript@${typeScriptVersion}/lib`,
234+
jsdelivr: `https://cdn.jsdelivr.net/npm/typescript@${typeScriptVersion}/lib`,
234235
}
235236

236-
const typeScriptLibStableFallbackBase = `https://cdn.jsdelivr.net/npm/typescript@${typeScriptVersion}/lib`
237+
/*
238+
* Keep a reliable fallback order for .d.ts files when the active module provider
239+
* does not host TypeScript lib declarations consistently (e.g. import maps/jspmGa).
240+
*/
241+
const typeScriptLibFallbackProviderPriority = ['jsdelivr', 'unpkg', 'esm']
237242

238-
export const getTypeScriptLibUrls = fileName => {
239-
const providerOrderedBases = getProviderPriority()
240-
.map(provider => typeScriptLibBaseByProvider[provider])
241-
.filter(Boolean)
243+
const getTypeScriptLibProviderPriority = typeScriptProvider => {
244+
const ordered = []
245+
246+
if (
247+
typeof typeScriptProvider === 'string' &&
248+
typeScriptProvider in typeScriptLibBaseByProvider
249+
) {
250+
ordered.push(typeScriptProvider)
251+
}
242252

243-
if (!providerOrderedBases.includes(typeScriptLibStableFallbackBase)) {
244-
providerOrderedBases.push(typeScriptLibStableFallbackBase)
253+
for (const provider of typeScriptLibFallbackProviderPriority) {
254+
ordered.push(provider)
245255
}
246256

257+
return [...new Set(ordered)]
258+
}
259+
260+
export const getTypeScriptLibUrls = (fileName, { typeScriptProvider } = {}) => {
261+
const providerOrderedBases = getTypeScriptLibProviderPriority(typeScriptProvider)
262+
.map(provider => typeScriptLibBaseByProvider[provider])
263+
.filter(Boolean)
264+
247265
return providerOrderedBases.map(baseUrl => `${baseUrl}/${fileName}`)
248266
}

src/styles.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,11 @@ textarea:focus {
584584
background: var(--surface-control-hover);
585585
}
586586

587+
.diagnostics-toggle:focus-visible {
588+
outline: 2px solid var(--focus-ring);
589+
outline-offset: 2px;
590+
}
591+
587592
.diagnostics-toggle--neutral {
588593
border-color: var(--border-control);
589594
background: var(--surface-control);

0 commit comments

Comments
 (0)