Skip to content

Commit 365dc29

Browse files
fix: require App in entry tab.
1 parent 8ae4b9a commit 365dc29

5 files changed

Lines changed: 54 additions & 175 deletions

File tree

playwright/helpers/app-test-helpers.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,22 @@ export const openWorkspaceTab = async (page: Page, fileName: string) => {
171171
await page.getByRole('button', { name: pattern }).click()
172172
}
173173

174+
const replaceEditorSource = async ({
175+
editorContent,
176+
source,
177+
}: {
178+
editorContent: ReturnType<Page['locator']>
179+
source: string
180+
}) => {
181+
for (let attempt = 0; attempt < 2; attempt += 1) {
182+
await editorContent.fill('')
183+
await editorContent.fill(source)
184+
await editorContent.press('End')
185+
await editorContent.type(' ')
186+
await editorContent.press('Backspace')
187+
}
188+
}
189+
174190
export const reorderWorkspaceTabBefore = async (
175191
page: Page,
176192
{ from, to }: { from: string; to: string },
@@ -199,35 +215,29 @@ export const setWorkspaceTabSource = async (
199215
},
200216
) => {
201217
await openWorkspaceTab(page, fileName)
218+
await expect(page.getByRole('region', { name: fileName })).toBeVisible()
202219
const editorContent = page
203220
.locator(`.editor-panel[data-editor-kind="${kind}"] .cm-content`)
204221
.first()
205-
await editorContent.fill(source)
206-
await editorContent.press('End')
207-
await editorContent.type(' ')
208-
await editorContent.press('Backspace')
222+
await replaceEditorSource({ editorContent, source })
209223
}
210224

211225
export const setComponentEditorSource = async (page: Page, source: string) => {
212226
await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
227+
await expect(page.getByRole('region', { name: 'App.tsx' })).toBeVisible()
213228
const editorContent = page
214229
.locator('.editor-panel[data-editor-kind="component"] .cm-content')
215230
.first()
216-
await editorContent.fill(source)
217-
await editorContent.press('End')
218-
await editorContent.type(' ')
219-
await editorContent.press('Backspace')
231+
await replaceEditorSource({ editorContent, source })
220232
}
221233

222234
export const setStylesEditorSource = async (page: Page, source: string) => {
223235
await page.getByRole('button', { name: 'Open tab app.css' }).click()
236+
await expect(page.getByRole('region', { name: 'app.css' })).toBeVisible()
224237
const editorContent = page
225238
.locator('.editor-panel[data-editor-kind="styles"] .cm-content')
226239
.first()
227-
await editorContent.fill(source)
228-
await editorContent.press('End')
229-
await editorContent.type(' ')
230-
await editorContent.press('Backspace')
240+
await replaceEditorSource({ editorContent, source })
231241
}
232242

233243
export const getActiveComponentEditorLineNumber = async (page: Page) => {

playwright/rendering-modes.spec.ts

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ test('shows App-only error when auto render is disabled and App is missing', asy
577577
)
578578
})
579579

580-
test('auto render implicitly wraps source with App in dom and react modes', async ({
580+
test('auto render shows App-only error in dom and react modes when App is missing', async ({
581581
page,
582582
}) => {
583583
await waitForInitialRender(page)
@@ -589,9 +589,9 @@ test('auto render implicitly wraps source with App in dom and react modes', asyn
589589
'const Button = () => <button type="button">implicit app dom</button>',
590590
)
591591

592-
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
593-
await expect(getPreviewFrame(page).getByRole('button')).toContainText(
594-
'implicit app dom',
592+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
593+
await expect(page.locator('#preview-host pre')).toContainText(
594+
'Expected a function or const named App.',
595595
)
596596

597597
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
@@ -604,13 +604,13 @@ test('auto render implicitly wraps source with App in dom and react modes', asyn
604604
page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(),
605605
).toContainText('implicit app react')
606606

607-
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
608-
await expect(getPreviewFrame(page).getByRole('button')).toContainText(
609-
'implicit app react',
607+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
608+
await expect(page.locator('#preview-host pre')).toContainText(
609+
'Expected a function or const named App.',
610610
)
611611
})
612612

613-
test('auto render implicit App includes multiple component declarations', async ({
613+
test('auto render renders successfully when explicit App is defined in dom and react modes', async ({
614614
page,
615615
}) => {
616616
await waitForInitialRender(page)
@@ -619,15 +619,25 @@ test('auto render implicit App includes multiple component declarations', async
619619

620620
await setComponentEditorSource(
621621
page,
622-
[
623-
'const OtherButton = () => <button type="button">bar</button>',
624-
'const Button = () => <button type="button">foo</button>',
625-
].join('\n'),
622+
'const App = () => <button type="button">explicit app dom</button>',
626623
)
627624

628625
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
629-
await expect(getPreviewFrame(page).getByRole('button')).toHaveCount(2)
630-
await expect(getPreviewFrame(page).getByRole('button')).toContainText(['bar', 'foo'])
626+
await expect(getPreviewFrame(page).getByRole('button')).toContainText(
627+
'explicit app dom',
628+
)
629+
630+
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
631+
await expect(page.getByRole('combobox', { name: 'Render mode' })).toHaveValue('react')
632+
await setComponentEditorSource(
633+
page,
634+
'const App = () => <button type="button">explicit app react</button>',
635+
)
636+
637+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
638+
await expect(getPreviewFrame(page).getByRole('button')).toContainText(
639+
'explicit app react',
640+
)
631641
})
632642

633643
test('auto render does not treat lowercase helpers as implicit components', async ({
@@ -651,7 +661,7 @@ test('auto render does not treat lowercase helpers as implicit components', asyn
651661
)
652662
})
653663

654-
test('auto render wraps standalone JSX with trailing semicolon and comment', async ({
664+
test('auto render shows App-only error for standalone JSX expression', async ({
655665
page,
656666
}) => {
657667
await waitForInitialRender(page)
@@ -663,13 +673,13 @@ test('auto render wraps standalone JSX with trailing semicolon and comment', asy
663673
'(<button type="button">implicit app from jsx expression</button>) as any; // trailing',
664674
)
665675

666-
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
667-
await expect(getPreviewFrame(page).getByRole('button')).toContainText(
668-
'implicit app from jsx expression',
676+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
677+
await expect(page.locator('#preview-host pre')).toContainText(
678+
'Expected a function or const named App.',
669679
)
670680
})
671681

672-
test('auto render requires explicit App for declarations plus top-level JSX expression', async ({
682+
test('auto render shows App-only error for declarations plus top-level JSX expression', async ({
673683
page,
674684
}) => {
675685
await waitForInitialRender(page)
@@ -687,7 +697,7 @@ test('auto render requires explicit App for declarations plus top-level JSX expr
687697

688698
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
689699
await expect(page.locator('#preview-host pre')).toContainText(
690-
'Top-level JSX with declarations or imports requires an explicit App component.',
700+
'Expected a function or const named App.',
691701
)
692702
})
693703

src/modules/app-core/workspace-controllers-setup.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,6 @@ const createWorkspaceControllersSetup = ({
111111
const setActiveWorkspaceTab = tabId =>
112112
workspaceTabSelectionController.setActiveWorkspaceTab(tabId)
113113

114-
const syncEditorFromActiveWorkspaceTabDelegate = () =>
115-
workspaceTabSelectionController.syncEditorFromActiveWorkspaceTab()
116-
117114
workspaceTabMutationsController = createWorkspaceTabMutationsController({
118115
toNonEmptyWorkspaceText,
119116
workspaceTabsState,
@@ -186,7 +183,6 @@ const createWorkspaceControllersSetup = ({
186183
getShouldShowEditedDesign,
187184
workspaceTabsShell,
188185
workspaceTabAddWrap,
189-
syncEditorFromActiveWorkspaceTab: syncEditorFromActiveWorkspaceTabDelegate,
190186
})
191187

192188
const workspaceContextController = createWorkspaceContextController({

src/modules/app-core/workspace-tabs-renderer.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const createWorkspaceTabsRenderer = ({
2222
getShouldShowEditedDesign,
2323
workspaceTabsShell,
2424
workspaceTabAddWrap,
25-
syncEditorFromActiveWorkspaceTab,
2625
}) => {
2726
const clearWorkspaceTabDragState = () => {
2827
setDraggedWorkspaceTabId('')
@@ -294,8 +293,6 @@ const createWorkspaceTabsRenderer = ({
294293
renderWorkspaceTabs()
295294
return
296295
}
297-
298-
syncEditorFromActiveWorkspaceTab()
299296
}
300297

301298
return {

src/modules/preview/render-runtime.js

Lines changed: 2 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
import {
2-
collectTopLevelTransformMetadata,
3-
getFunctionLikeDeclarationNames,
4-
hasFunctionLikeDeclarationNamed,
5-
} from './jsx-top-level-declarations.js'
61
import { canRenderPreview, resolvePreviewEntryTab } from './preview-entry-resolver.js'
72
import { createWorkspaceIframePreviewBridge } from '../preview-runtime/iframe-preview-executor.js'
83
import { planWorkspaceVirtualModules } from '../preview-runtime/virtual-workspace-modules.js'
@@ -14,7 +9,7 @@ export const createRenderRuntimeController = ({
149
cdnImports,
1510
importFromCdnWithFallback,
1611
renderMode,
17-
isAutoRenderEnabled = () => false,
12+
isAutoRenderEnabled: _isAutoRenderEnabled = () => false,
1813
getJsxSource,
1914
getWorkspaceTabs,
2015
getPreviewHost,
@@ -47,11 +42,6 @@ export const createRenderRuntimeController = ({
4742
let iframeRuntimeBridge = null
4843
let lastRenderedEntryTabId = ''
4944
let lastRenderedDependencyTabIds = new Set()
50-
let topLevelTransformMetadataCache = {
51-
source: null,
52-
transformJsxSource: null,
53-
value: null,
54-
}
5545
let hasCompletedInitialRender = false
5646
const workspaceGraphCache = createPreviewWorkspaceGraphCache()
5747
const styleTabLanguages = new Set(['css', 'less', 'sass', 'module'])
@@ -277,104 +267,6 @@ export const createRenderRuntimeController = ({
277267
return lines.join('\n')
278268
}
279269

280-
const hasAppDeclaration = declarations =>
281-
hasFunctionLikeDeclarationNamed({ declarations, name: 'App' })
282-
283-
const isComponentLikeName = name => typeof name === 'string' && /^[A-Z]/.test(name)
284-
285-
const getComponentNames = declarations =>
286-
getFunctionLikeDeclarationNames({ declarations, excludeNames: ['App'] }).filter(
287-
isComponentLikeName,
288-
)
289-
290-
const isSourceRange = range =>
291-
Array.isArray(range) &&
292-
range.length === 2 &&
293-
Number.isInteger(range[0]) &&
294-
Number.isInteger(range[1])
295-
296-
const sourceFromRange = ({ source, range }) => {
297-
if (!isSourceRange(range)) {
298-
return null
299-
}
300-
301-
const [start, end] = range
302-
if (start < 0 || end < start || end > source.length) {
303-
return null
304-
}
305-
306-
const expression = source.slice(start, end).trim()
307-
return expression || null
308-
}
309-
310-
const getTopLevelTransformMetadata = ({ source, transformJsxSource }) => {
311-
if (
312-
topLevelTransformMetadataCache.source === source &&
313-
topLevelTransformMetadataCache.transformJsxSource === transformJsxSource &&
314-
topLevelTransformMetadataCache.value
315-
) {
316-
return topLevelTransformMetadataCache.value
317-
}
318-
319-
const value = collectTopLevelTransformMetadata({ source, transformJsxSource })
320-
topLevelTransformMetadataCache = {
321-
source,
322-
transformJsxSource,
323-
value,
324-
}
325-
326-
return value
327-
}
328-
329-
const withImplicitAppWrapper = (source, transformJsxSource) => {
330-
if (!source.trim()) {
331-
return source
332-
}
333-
334-
if (/^\s*export\s+default\b/m.test(source)) {
335-
return source
336-
}
337-
338-
const {
339-
declarations,
340-
importCount,
341-
hasTopLevelJsxExpression,
342-
topLevelJsxExpressionRange,
343-
} = getTopLevelTransformMetadata({ source, transformJsxSource })
344-
if (hasAppDeclaration(declarations)) {
345-
return source
346-
}
347-
348-
if (hasTopLevelJsxExpression) {
349-
const expressionSource = sourceFromRange({
350-
source,
351-
range: topLevelJsxExpressionRange,
352-
})
353-
354-
if (!expressionSource) {
355-
throw new Error(
356-
'Unable to infer top-level JSX entry for implicit App. Define App explicitly.',
357-
)
358-
}
359-
360-
if (declarations.length > 0 || importCount > 0) {
361-
throw new Error(
362-
'Top-level JSX with declarations or imports requires an explicit App component.',
363-
)
364-
}
365-
366-
return `const App = () => (${expressionSource})`
367-
}
368-
369-
const componentNames = getComponentNames(declarations)
370-
if (componentNames.length > 0) {
371-
const children = componentNames.map(name => ` <${name} />`).join('\n')
372-
return `${source}\n\nconst App = () => (\n <>\n${children}\n </>\n)`
373-
}
374-
375-
return source
376-
}
377-
378270
const isSassCompiler = candidate =>
379271
Boolean(
380272
candidate &&
@@ -765,28 +657,6 @@ export const createRenderRuntimeController = ({
765657
reactDomClient: getRuntimeSpecifier('reactDomClient'),
766658
})
767659

768-
const withPreparedEntrySource = ({ tabs, entryTab, transformJsxSource }) => {
769-
if (!isAutoRenderEnabled()) {
770-
return tabs
771-
}
772-
773-
const entrySource = typeof entryTab?.content === 'string' ? entryTab.content : ''
774-
const wrappedEntrySource = withImplicitAppWrapper(entrySource, transformJsxSource)
775-
776-
if (wrappedEntrySource === entrySource) {
777-
return tabs
778-
}
779-
780-
return tabs.map(tab =>
781-
tab?.id === entryTab.id
782-
? {
783-
...tab,
784-
content: wrappedEntrySource,
785-
}
786-
: tab,
787-
)
788-
}
789-
790660
const renderWorkspaceInIframe = async ({ mode, cssText }) => {
791661
const workspaceTabs = getWorkspaceTabsForPreview()
792662
const entryTab = resolveWorkspaceEntryTab(workspaceTabs)
@@ -796,11 +666,7 @@ export const createRenderRuntimeController = ({
796666
}
797667

798668
const { transformJsxSource } = await ensureCoreRuntime()
799-
const tabsForExecution = withPreparedEntrySource({
800-
tabs: workspaceTabs,
801-
entryTab,
802-
transformJsxSource,
803-
})
669+
const tabsForExecution = workspaceTabs
804670
const entryTabForExecution =
805671
resolvePreviewEntryTab(tabsForExecution) ??
806672
tabsForExecution.find(tab => tab?.id === entryTab.id) ??

0 commit comments

Comments
 (0)