From 365dc2971e1c3b5e8a5521023fc428d2128620b1 Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 19 Apr 2026 12:55:53 -0500 Subject: [PATCH 1/5] fix: require App in entry tab. --- playwright/helpers/app-test-helpers.ts | 34 +++-- playwright/rendering-modes.spec.ts | 50 ++++--- .../app-core/workspace-controllers-setup.js | 4 - .../app-core/workspace-tabs-renderer.js | 3 - src/modules/preview/render-runtime.js | 138 +----------------- 5 files changed, 54 insertions(+), 175 deletions(-) diff --git a/playwright/helpers/app-test-helpers.ts b/playwright/helpers/app-test-helpers.ts index 9224765..c8763a5 100644 --- a/playwright/helpers/app-test-helpers.ts +++ b/playwright/helpers/app-test-helpers.ts @@ -171,6 +171,22 @@ export const openWorkspaceTab = async (page: Page, fileName: string) => { await page.getByRole('button', { name: pattern }).click() } +const replaceEditorSource = async ({ + editorContent, + source, +}: { + editorContent: ReturnType + source: string +}) => { + for (let attempt = 0; attempt < 2; attempt += 1) { + await editorContent.fill('') + await editorContent.fill(source) + await editorContent.press('End') + await editorContent.type(' ') + await editorContent.press('Backspace') + } +} + export const reorderWorkspaceTabBefore = async ( page: Page, { from, to }: { from: string; to: string }, @@ -199,35 +215,29 @@ export const setWorkspaceTabSource = async ( }, ) => { await openWorkspaceTab(page, fileName) + await expect(page.getByRole('region', { name: fileName })).toBeVisible() const editorContent = page .locator(`.editor-panel[data-editor-kind="${kind}"] .cm-content`) .first() - await editorContent.fill(source) - await editorContent.press('End') - await editorContent.type(' ') - await editorContent.press('Backspace') + await replaceEditorSource({ editorContent, source }) } export const setComponentEditorSource = async (page: Page, source: string) => { await page.getByRole('button', { name: 'Open tab App.tsx' }).click() + await expect(page.getByRole('region', { name: 'App.tsx' })).toBeVisible() const editorContent = page .locator('.editor-panel[data-editor-kind="component"] .cm-content') .first() - await editorContent.fill(source) - await editorContent.press('End') - await editorContent.type(' ') - await editorContent.press('Backspace') + await replaceEditorSource({ editorContent, source }) } export const setStylesEditorSource = async (page: Page, source: string) => { await page.getByRole('button', { name: 'Open tab app.css' }).click() + await expect(page.getByRole('region', { name: 'app.css' })).toBeVisible() const editorContent = page .locator('.editor-panel[data-editor-kind="styles"] .cm-content') .first() - await editorContent.fill(source) - await editorContent.press('End') - await editorContent.type(' ') - await editorContent.press('Backspace') + await replaceEditorSource({ editorContent, source }) } export const getActiveComponentEditorLineNumber = async (page: Page) => { diff --git a/playwright/rendering-modes.spec.ts b/playwright/rendering-modes.spec.ts index 2335e4d..b51aa85 100644 --- a/playwright/rendering-modes.spec.ts +++ b/playwright/rendering-modes.spec.ts @@ -577,7 +577,7 @@ test('shows App-only error when auto render is disabled and App is missing', asy ) }) -test('auto render implicitly wraps source with App in dom and react modes', async ({ +test('auto render shows App-only error in dom and react modes when App is missing', async ({ page, }) => { await waitForInitialRender(page) @@ -589,9 +589,9 @@ test('auto render implicitly wraps source with App in dom and react modes', asyn 'const Button = () => ', ) - await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') - await expect(getPreviewFrame(page).getByRole('button')).toContainText( - 'implicit app dom', + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') + await expect(page.locator('#preview-host pre')).toContainText( + 'Expected a function or const named App.', ) 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 page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(), ).toContainText('implicit app react') - await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') - await expect(getPreviewFrame(page).getByRole('button')).toContainText( - 'implicit app react', + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') + await expect(page.locator('#preview-host pre')).toContainText( + 'Expected a function or const named App.', ) }) -test('auto render implicit App includes multiple component declarations', async ({ +test('auto render renders successfully when explicit App is defined in dom and react modes', async ({ page, }) => { await waitForInitialRender(page) @@ -619,15 +619,25 @@ test('auto render implicit App includes multiple component declarations', async await setComponentEditorSource( page, - [ - 'const OtherButton = () => ', - 'const Button = () => ', - ].join('\n'), + 'const App = () => ', ) await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') - await expect(getPreviewFrame(page).getByRole('button')).toHaveCount(2) - await expect(getPreviewFrame(page).getByRole('button')).toContainText(['bar', 'foo']) + await expect(getPreviewFrame(page).getByRole('button')).toContainText( + 'explicit app dom', + ) + + await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react') + await expect(page.getByRole('combobox', { name: 'Render mode' })).toHaveValue('react') + await setComponentEditorSource( + page, + 'const App = () => ', + ) + + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') + await expect(getPreviewFrame(page).getByRole('button')).toContainText( + 'explicit app react', + ) }) 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 ) }) -test('auto render wraps standalone JSX with trailing semicolon and comment', async ({ +test('auto render shows App-only error for standalone JSX expression', async ({ page, }) => { await waitForInitialRender(page) @@ -663,13 +673,13 @@ test('auto render wraps standalone JSX with trailing semicolon and comment', asy '() as any; // trailing', ) - await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') - await expect(getPreviewFrame(page).getByRole('button')).toContainText( - 'implicit app from jsx expression', + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') + await expect(page.locator('#preview-host pre')).toContainText( + 'Expected a function or const named App.', ) }) -test('auto render requires explicit App for declarations plus top-level JSX expression', async ({ +test('auto render shows App-only error for declarations plus top-level JSX expression', async ({ page, }) => { await waitForInitialRender(page) @@ -687,7 +697,7 @@ test('auto render requires explicit App for declarations plus top-level JSX expr await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') await expect(page.locator('#preview-host pre')).toContainText( - 'Top-level JSX with declarations or imports requires an explicit App component.', + 'Expected a function or const named App.', ) }) diff --git a/src/modules/app-core/workspace-controllers-setup.js b/src/modules/app-core/workspace-controllers-setup.js index ec09b78..ed4919d 100644 --- a/src/modules/app-core/workspace-controllers-setup.js +++ b/src/modules/app-core/workspace-controllers-setup.js @@ -111,9 +111,6 @@ const createWorkspaceControllersSetup = ({ const setActiveWorkspaceTab = tabId => workspaceTabSelectionController.setActiveWorkspaceTab(tabId) - const syncEditorFromActiveWorkspaceTabDelegate = () => - workspaceTabSelectionController.syncEditorFromActiveWorkspaceTab() - workspaceTabMutationsController = createWorkspaceTabMutationsController({ toNonEmptyWorkspaceText, workspaceTabsState, @@ -186,7 +183,6 @@ const createWorkspaceControllersSetup = ({ getShouldShowEditedDesign, workspaceTabsShell, workspaceTabAddWrap, - syncEditorFromActiveWorkspaceTab: syncEditorFromActiveWorkspaceTabDelegate, }) const workspaceContextController = createWorkspaceContextController({ diff --git a/src/modules/app-core/workspace-tabs-renderer.js b/src/modules/app-core/workspace-tabs-renderer.js index a47369f..09c6937 100644 --- a/src/modules/app-core/workspace-tabs-renderer.js +++ b/src/modules/app-core/workspace-tabs-renderer.js @@ -22,7 +22,6 @@ const createWorkspaceTabsRenderer = ({ getShouldShowEditedDesign, workspaceTabsShell, workspaceTabAddWrap, - syncEditorFromActiveWorkspaceTab, }) => { const clearWorkspaceTabDragState = () => { setDraggedWorkspaceTabId('') @@ -294,8 +293,6 @@ const createWorkspaceTabsRenderer = ({ renderWorkspaceTabs() return } - - syncEditorFromActiveWorkspaceTab() } return { diff --git a/src/modules/preview/render-runtime.js b/src/modules/preview/render-runtime.js index 33dfcd3..c4deac6 100644 --- a/src/modules/preview/render-runtime.js +++ b/src/modules/preview/render-runtime.js @@ -1,8 +1,3 @@ -import { - collectTopLevelTransformMetadata, - getFunctionLikeDeclarationNames, - hasFunctionLikeDeclarationNamed, -} from './jsx-top-level-declarations.js' import { canRenderPreview, resolvePreviewEntryTab } from './preview-entry-resolver.js' import { createWorkspaceIframePreviewBridge } from '../preview-runtime/iframe-preview-executor.js' import { planWorkspaceVirtualModules } from '../preview-runtime/virtual-workspace-modules.js' @@ -14,7 +9,7 @@ export const createRenderRuntimeController = ({ cdnImports, importFromCdnWithFallback, renderMode, - isAutoRenderEnabled = () => false, + isAutoRenderEnabled: _isAutoRenderEnabled = () => false, getJsxSource, getWorkspaceTabs, getPreviewHost, @@ -47,11 +42,6 @@ export const createRenderRuntimeController = ({ let iframeRuntimeBridge = null let lastRenderedEntryTabId = '' let lastRenderedDependencyTabIds = new Set() - let topLevelTransformMetadataCache = { - source: null, - transformJsxSource: null, - value: null, - } let hasCompletedInitialRender = false const workspaceGraphCache = createPreviewWorkspaceGraphCache() const styleTabLanguages = new Set(['css', 'less', 'sass', 'module']) @@ -277,104 +267,6 @@ export const createRenderRuntimeController = ({ return lines.join('\n') } - const hasAppDeclaration = declarations => - hasFunctionLikeDeclarationNamed({ declarations, name: 'App' }) - - const isComponentLikeName = name => typeof name === 'string' && /^[A-Z]/.test(name) - - const getComponentNames = declarations => - getFunctionLikeDeclarationNames({ declarations, excludeNames: ['App'] }).filter( - isComponentLikeName, - ) - - const isSourceRange = range => - Array.isArray(range) && - range.length === 2 && - Number.isInteger(range[0]) && - Number.isInteger(range[1]) - - const sourceFromRange = ({ source, range }) => { - if (!isSourceRange(range)) { - return null - } - - const [start, end] = range - if (start < 0 || end < start || end > source.length) { - return null - } - - const expression = source.slice(start, end).trim() - return expression || null - } - - const getTopLevelTransformMetadata = ({ source, transformJsxSource }) => { - if ( - topLevelTransformMetadataCache.source === source && - topLevelTransformMetadataCache.transformJsxSource === transformJsxSource && - topLevelTransformMetadataCache.value - ) { - return topLevelTransformMetadataCache.value - } - - const value = collectTopLevelTransformMetadata({ source, transformJsxSource }) - topLevelTransformMetadataCache = { - source, - transformJsxSource, - value, - } - - return value - } - - const withImplicitAppWrapper = (source, transformJsxSource) => { - if (!source.trim()) { - return source - } - - if (/^\s*export\s+default\b/m.test(source)) { - return source - } - - const { - declarations, - importCount, - hasTopLevelJsxExpression, - topLevelJsxExpressionRange, - } = getTopLevelTransformMetadata({ source, transformJsxSource }) - if (hasAppDeclaration(declarations)) { - return source - } - - if (hasTopLevelJsxExpression) { - const expressionSource = sourceFromRange({ - source, - range: topLevelJsxExpressionRange, - }) - - if (!expressionSource) { - throw new Error( - 'Unable to infer top-level JSX entry for implicit App. Define App explicitly.', - ) - } - - if (declarations.length > 0 || importCount > 0) { - throw new Error( - 'Top-level JSX with declarations or imports requires an explicit App component.', - ) - } - - return `const App = () => (${expressionSource})` - } - - const componentNames = getComponentNames(declarations) - if (componentNames.length > 0) { - const children = componentNames.map(name => ` <${name} />`).join('\n') - return `${source}\n\nconst App = () => (\n <>\n${children}\n \n)` - } - - return source - } - const isSassCompiler = candidate => Boolean( candidate && @@ -765,28 +657,6 @@ export const createRenderRuntimeController = ({ reactDomClient: getRuntimeSpecifier('reactDomClient'), }) - const withPreparedEntrySource = ({ tabs, entryTab, transformJsxSource }) => { - if (!isAutoRenderEnabled()) { - return tabs - } - - const entrySource = typeof entryTab?.content === 'string' ? entryTab.content : '' - const wrappedEntrySource = withImplicitAppWrapper(entrySource, transformJsxSource) - - if (wrappedEntrySource === entrySource) { - return tabs - } - - return tabs.map(tab => - tab?.id === entryTab.id - ? { - ...tab, - content: wrappedEntrySource, - } - : tab, - ) - } - const renderWorkspaceInIframe = async ({ mode, cssText }) => { const workspaceTabs = getWorkspaceTabsForPreview() const entryTab = resolveWorkspaceEntryTab(workspaceTabs) @@ -796,11 +666,7 @@ export const createRenderRuntimeController = ({ } const { transformJsxSource } = await ensureCoreRuntime() - const tabsForExecution = withPreparedEntrySource({ - tabs: workspaceTabs, - entryTab, - transformJsxSource, - }) + const tabsForExecution = workspaceTabs const entryTabForExecution = resolvePreviewEntryTab(tabsForExecution) ?? tabsForExecution.find(tab => tab?.id === entryTab.id) ?? From 7c3b5aea545f6e24a434e44aa2d992e59fb237a2 Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 19 Apr 2026 13:09:41 -0500 Subject: [PATCH 2/5] fix: hide workspaces until pat provided. --- src/index.html | 1 + src/modules/app-core/github-pr-context-ui.js | 28 +++++++++++++++----- src/modules/app-core/github-workflows.js | 3 --- src/styles/ai-controls.css | 6 +++++ src/styles/diagnostics.css | 4 +++ 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/index.html b/src/index.html index d625d9a..f3c62eb 100644 --- a/src/index.html +++ b/src/index.html @@ -165,6 +165,7 @@

aria-controls="workspaces-drawer" title="Manage local workspaces" disabled + hidden >