Skip to content

Commit 4c0da02

Browse files
fix: import diagnostics in preview and css modules default imports.
1 parent 2da1ab3 commit 4c0da02

4 files changed

Lines changed: 369 additions & 39 deletions

File tree

playwright/rendering-modes/core.spec.ts

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,127 @@ import {
1313
waitForInitialRender,
1414
} from '../helpers/app-test-helpers.js'
1515

16+
const renameWorkspaceTab = async (
17+
page: import('@playwright/test').Page,
18+
{
19+
from,
20+
to,
21+
}: {
22+
from: string
23+
to: string
24+
},
25+
) => {
26+
await page.getByRole('button', { name: `Rename tab ${from}` }).click()
27+
const renameInput = page.getByLabel(`Rename ${from}`)
28+
await renameInput.fill(to)
29+
await renameInput.press('Enter')
30+
}
31+
32+
const renameWorkspaceTabFromCandidates = async (
33+
page: import('@playwright/test').Page,
34+
{
35+
fromCandidates,
36+
to,
37+
}: {
38+
fromCandidates: string[]
39+
to: string
40+
},
41+
) => {
42+
for (const from of fromCandidates) {
43+
const button = page.getByRole('button', { name: `Rename tab ${from}` })
44+
if ((await button.count()) === 0) {
45+
continue
46+
}
47+
48+
await button.click()
49+
const renameInput = page.getByLabel(`Rename ${from}`)
50+
await renameInput.fill(to)
51+
await renameInput.press('Enter')
52+
return
53+
}
54+
55+
throw new Error(
56+
`Could not find a rename target from candidates: ${fromCandidates.join(', ')}`,
57+
)
58+
}
59+
60+
const readLatestWorkspaceSnapshot = async (page: import('@playwright/test').Page) => {
61+
return page.evaluate(async () => {
62+
const dbName = 'knighted-develop-workspaces'
63+
const storeName = 'prWorkspaces'
64+
65+
const openDb = await new Promise<IDBDatabase | null>(resolve => {
66+
try {
67+
const request = indexedDB.open(dbName)
68+
request.onsuccess = () => resolve(request.result)
69+
request.onerror = () => resolve(null)
70+
} catch {
71+
resolve(null)
72+
}
73+
})
74+
75+
if (!openDb) {
76+
return null
77+
}
78+
79+
const records = await new Promise<Array<Record<string, unknown>>>(resolve => {
80+
try {
81+
const transaction = openDb.transaction(storeName, 'readonly')
82+
const store = transaction.objectStore(storeName)
83+
const request = store.getAll()
84+
request.onsuccess = () => {
85+
const value = Array.isArray(request.result) ? request.result : []
86+
resolve(value as Array<Record<string, unknown>>)
87+
}
88+
request.onerror = () => resolve([])
89+
} catch {
90+
resolve([])
91+
}
92+
})
93+
94+
openDb.close()
95+
96+
if (!Array.isArray(records) || records.length === 0) {
97+
return null
98+
}
99+
100+
const sorted = [...records].sort((a, b) => {
101+
const first =
102+
typeof a.lastModified === 'number' && Number.isFinite(a.lastModified)
103+
? a.lastModified
104+
: 0
105+
const second =
106+
typeof b.lastModified === 'number' && Number.isFinite(b.lastModified)
107+
? b.lastModified
108+
: 0
109+
return second - first
110+
})
111+
112+
const latest = sorted[0] ?? {}
113+
const tabs = Array.isArray(latest.tabs) ? latest.tabs : []
114+
const primaryStylesTab = tabs.find(tab => {
115+
if (!tab || typeof tab !== 'object') {
116+
return false
117+
}
118+
119+
const tabRecord = tab as Record<string, unknown>
120+
const language = typeof tabRecord.language === 'string' ? tabRecord.language : ''
121+
const path = typeof tabRecord.path === 'string' ? tabRecord.path : ''
122+
const name = typeof tabRecord.name === 'string' ? tabRecord.name : ''
123+
const isStyleLanguage = ['css', 'less', 'sass', 'module'].includes(language)
124+
const styleIdentity = `${path} ${name}`.toLowerCase()
125+
const looksLikeStyle = /\.(css|less|sass|scss)\b/.test(styleIdentity)
126+
return isStyleLanguage && looksLikeStyle
127+
}) as Record<string, unknown> | undefined
128+
129+
return {
130+
renderMode: typeof latest.renderMode === 'string' ? latest.renderMode : '',
131+
styleLanguage:
132+
typeof primaryStylesTab?.language === 'string' ? primaryStylesTab.language : '',
133+
}
134+
})
135+
}
136+
16137
test.beforeEach(async ({ page }) => {
17138
await resetWorkbenchStorage(page)
18139
})
@@ -31,6 +152,95 @@ test('renders in react mode with css modules', async ({ page }) => {
31152
await expectPreviewHasRenderedContent(page)
32153
})
33154

155+
test('css module imports expose class map for module tabs', async ({ page }) => {
156+
await waitForInitialRender(page)
157+
158+
await ensurePanelToolsVisible(page, 'component')
159+
160+
await renameWorkspaceTab(page, {
161+
from: 'app.css',
162+
to: 'app.module.css',
163+
})
164+
165+
await setWorkspaceTabSource(page, {
166+
fileName: 'app.module.css',
167+
kind: 'styles',
168+
source: [
169+
'.list {',
170+
' display: grid;',
171+
'}',
172+
'',
173+
'.item {',
174+
' color: rgb(10, 20, 30);',
175+
'}',
176+
].join('\n'),
177+
})
178+
179+
await addWorkspaceTab(page, { type: 'script' })
180+
await renameWorkspaceTab(page, {
181+
from: 'module.tsx',
182+
to: 'list.tsx',
183+
})
184+
await setWorkspaceTabSource(page, {
185+
fileName: 'list.tsx',
186+
source: [
187+
"import styles from '../styles/app.module.css'",
188+
"import { Item } from './item'",
189+
'',
190+
'type ListProps = {',
191+
' items: string[]',
192+
'}',
193+
'',
194+
'export const List = ({ items }: ListProps) => (',
195+
' <ul className={styles.list}>',
196+
' {items.map(item => (',
197+
' <Item key={item} value={item} />',
198+
' ))}',
199+
' </ul>',
200+
')',
201+
].join('\n'),
202+
})
203+
204+
await addWorkspaceTab(page, { type: 'script' })
205+
await renameWorkspaceTabFromCandidates(page, {
206+
fromCandidates: ['module-2.tsx', 'module.tsx', 'module-1.tsx'],
207+
to: 'item.tsx',
208+
})
209+
await setWorkspaceTabSource(page, {
210+
fileName: 'item.tsx',
211+
source: [
212+
"import styles from '../styles/app.module.css'",
213+
'',
214+
'type ItemProps = {',
215+
' value: string',
216+
'}',
217+
'',
218+
'export const Item = ({ value }: ItemProps) => (',
219+
' <li className={styles.item}>{value}</li>',
220+
')',
221+
].join('\n'),
222+
})
223+
224+
await setComponentEditorSource(
225+
page,
226+
[
227+
"import { List } from './list'",
228+
'',
229+
"const items = ['one', 'two']",
230+
'',
231+
'const App = () => <List items={items} />',
232+
].join('\n'),
233+
)
234+
235+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
236+
await expect(page.locator('#preview-host pre')).toHaveCount(0)
237+
await expect(getPreviewFrame(page).getByRole('listitem')).toHaveCount(2)
238+
await expect(getPreviewFrame(page).getByRole('listitem').first()).toHaveCSS(
239+
'color',
240+
'rgb(10, 20, 30)',
241+
)
242+
})
243+
34244
test('preview styles require explicit import from entry graph', async ({ page }) => {
35245
await waitForInitialRender(page)
36246

@@ -384,6 +594,29 @@ test('editing-transient missing reference runtime errors are suppressed', async
384594
await expect(page.getByRole('status', { name: 'App status' })).not.toHaveText('Error')
385595
})
386596

597+
test('missing component identifiers in App render as runtime errors', async ({
598+
page,
599+
}) => {
600+
await waitForInitialRender(page)
601+
await ensurePanelToolsVisible(page, 'component')
602+
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
603+
604+
await setComponentEditorSource(
605+
page,
606+
[
607+
'const App = () => (',
608+
' <List>',
609+
' <Item value="one" />',
610+
' </List>',
611+
')',
612+
].join('\n'),
613+
)
614+
615+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
616+
await expect(page.locator('#preview-host pre')).toContainText('[runtime]')
617+
await expect(page.locator('#preview-host pre')).toContainText('List is not defined')
618+
})
619+
387620
test('preview iframe sandbox isolates parent origin access', async ({ page }) => {
388621
await waitForInitialRender(page)
389622

@@ -706,6 +939,10 @@ test('persists render mode across reload', async ({ page }) => {
706939
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
707940
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
708941

942+
await expect
943+
.poll(async () => (await readLatestWorkspaceSnapshot(page))?.renderMode ?? '')
944+
.toBe('react')
945+
709946
await page.reload()
710947
await waitForInitialRender(page)
711948
await ensurePanelToolsVisible(page, 'component')
@@ -721,6 +958,10 @@ test('persists style mode across reload', async ({ page }) => {
721958
await expect(page.locator('#style-mode')).toHaveValue('sass')
722959
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
723960

961+
await expect
962+
.poll(async () => (await readLatestWorkspaceSnapshot(page))?.styleLanguage ?? '')
963+
.toBe('sass')
964+
724965
await page.reload()
725966
await waitForInitialRender(page)
726967

src/modules/preview-runtime/iframe-preview-executor.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,32 @@ const createIframeShellDocument = ({ channelId, parentOrigin, importMap }) => {
168168
}
169169
170170
const __knightedEmitRuntimeError = details => {
171-
const isMissingReference =
172-
typeof details.message === 'string' &&
173-
details.message.toLowerCase().includes(' is not defined')
171+
const missingReferenceName = (() => {
172+
if (typeof details.message !== 'string') {
173+
return ''
174+
}
175+
176+
const normalizedMessage = details.message.trim()
177+
const missingReferenceMatch = normalizedMessage.match(
178+
/^([A-Za-z_$][\\w$]*) is not defined\\b/i,
179+
)
180+
if (missingReferenceMatch?.[1]) {
181+
return missingReferenceMatch[1]
182+
}
183+
184+
const missingVariableMatch = normalizedMessage.match(
185+
/^can't find variable:\\s*([A-Za-z_$][\\w$]*)\\b/i,
186+
)
187+
return missingVariableMatch?.[1] ?? ''
188+
})()
189+
190+
const isLikelyTransientReference =
191+
missingReferenceName.length > 0 &&
192+
missingReferenceName.length <= 3 &&
193+
missingReferenceName === missingReferenceName.toLowerCase()
174194
const isTransientOrigin = details.origin === 'window-error' || details.origin === 'promise'
175-
if (isMissingReference && isTransientOrigin) {
195+
196+
if (isLikelyTransientReference && isTransientOrigin) {
176197
return
177198
}
178199

0 commit comments

Comments
 (0)