Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Repository structure:
- Preserve progressive loading behavior (lazy-load optional compilers/runtime pieces where possible).
- Do not introduce bundler-only assumptions into src/ runtime code.
- Prefer async/await over promise chains.
- Prefer const-assigned function expressions over function declarations, unless hoisting is explicitly required.
- Do not use IIFE, find another pattern instead.
- In Playwright tests, prefer accessible selectors first: `getByRole`, `getByLabel`, `getByText`, and explicit accessible names.
- Avoid `locator()` for interactive controls when a semantic selector is available.
Expand Down
2 changes: 1 addition & 1 deletion playwright/github-byot-ai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ test('BYOT remembers selected repository across reloads', async ({ page }) => {
await page.reload()
await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible()
await expect(page.getByRole('status', { name: 'App status' })).toHaveText(
'Loaded 2 writable repositories',
/Loaded 2 writable repositories|Rendered/,
{
timeout: 60_000,
},
Expand Down
31 changes: 31 additions & 0 deletions playwright/rendering-modes/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,37 @@ test('renders in react mode with css modules', async ({ page }) => {
await expectPreviewHasRenderedContent(page)
})

test('react mode keeps App.ts entry but surfaces rename guidance until compatible', async ({
page,
}) => {
await waitForInitialRender(page)
await ensurePanelToolsVisible(page, 'component')

await renameWorkspaceTab(page, {
from: 'App.tsx',
to: 'App.ts',
})

await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')

const expectedMessage =
'React mode requires the entry tab to end in .tsx, .jsx, or .js.'
await expect(page.getByRole('status', { name: 'App status' })).toContainText(
expectedMessage,
)
await expect(page.locator('#preview-host pre.preview-runtime-error')).toContainText(
expectedMessage,
)

await renameWorkspaceTab(page, {
from: 'App.ts',
to: 'App.jsx',
})

await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
await expectPreviewHasRenderedContent(page)
})

test('css module imports expose class map for module tabs', async ({ page }) => {
await waitForInitialRender(page)

Expand Down
60 changes: 25 additions & 35 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
setJsxSourceValue,
updateRenderModeEditability as updateRenderModeEditabilityValue,
} from './modules/app-core/runtime-editor-utils.js'
import { createSourceSetters } from './modules/app-core/source-setters.js'
import { createRuntimeCoreSetup } from './modules/app-core/runtime-core-setup.js'
import {
createWorkspaceContextSnapshotGetter,
Expand Down Expand Up @@ -61,6 +62,7 @@ import { createGitHubPrDrawer } from './modules/github/pr/drawer/controller/crea
import { createLayoutThemeController } from './modules/ui/layout-theme.js'
import { createLintDiagnosticsController } from './modules/diagnostics/lint-diagnostics.js'
import { createPreviewBackgroundController } from './modules/preview/preview-background.js'
import { getReactEntryTabCompatibilityError } from './modules/preview/preview-entry-resolver.js'
import { createRenderRuntimeController } from './modules/preview/render-runtime.js'
import { createTypeDiagnosticsController } from './modules/diagnostics/type-diagnostics.js'
import { collectTopLevelDeclarations } from './modules/preview/jsx-top-level-declarations.js'
Expand All @@ -80,6 +82,7 @@ import { createEnsureWorkspaceTabsShape } from './modules/workspace/workspace-ta
import {
createWorkspaceRecordId,
getDirtyStateForTabChange,
getAllowedEntryTabFileNames,
getPathFileName,
getTabKind,
getTabTargetPrFilePath,
Expand Down Expand Up @@ -211,7 +214,6 @@ const defaultComponentTabPath = 'src/components/App.tsx'
const defaultStylesTabPath = 'src/styles/app.css'
const defaultComponentTabName = 'App.tsx'
const defaultStylesTabName = 'app.css'
const allowedEntryTabFileNames = new Set(['app.tsx', 'app.js'])
const editorKinds = ['component', 'styles']
const editorPanelsByKind = {
component: componentEditorPanel,
Expand All @@ -237,6 +239,7 @@ let previewHost = document.getElementById('preview-host')
let jsxCodeEditor = null
let cssCodeEditor = null
let diagnosticsFlowController = null
let runtimeCore = null
let getJsxSource = () => jsxEditor.value
let getCssSource = () => cssEditor.value
let renderRuntime = null
Expand Down Expand Up @@ -282,6 +285,16 @@ let draggedWorkspaceTabId = ''
let dragOverWorkspaceTabId = ''
let suppressWorkspaceTabClick = false
const clipboardSupported = Boolean(navigator.clipboard?.writeText)
const { setJsxSource, setCssSource } = createSourceSetters({
setJsxSourceValue,
setCssSourceValue,
getJsxCodeEditor: () => jsxCodeEditor,
getCssCodeEditor: () => cssCodeEditor,
setSuppressEditorChangeSideEffects: nextValue =>
(suppressEditorChangeSideEffects = nextValue),
jsxEditor,
cssEditor,
})

const showAppToast = message => {
if (!(appToast instanceof HTMLElement)) {
Expand Down Expand Up @@ -695,6 +708,7 @@ const ensureWorkspaceTabsShape = createEnsureWorkspaceTabsShape({
defaultStylesTabPath,
defaultJsx,
normalizeEntryTabPath,
getAllowedEntryTabFileNames,
getPathFileName,
getTabTargetPrFilePath,
normalizeWorkspacePathValue,
Expand Down Expand Up @@ -791,7 +805,7 @@ const {
workspaceTabsShell,
workspaceTabAddWrap,
setWorkspaceTabRenameState: value => (workspaceTabRenameState = value),
allowedEntryTabFileNames,
getAllowedEntryTabFileNames,
getPathFileName,
normalizeEntryTabPath,
normalizeModuleTabPathForRename,
Expand Down Expand Up @@ -1191,8 +1205,8 @@ const githubWorkflows = createGitHubWorkflowsSetup({
githubPrContextClose,
},
actions: {
applyRenderMode,
applyStyleMode,
applyRenderMode: options => runtimeCore?.applyRenderMode(options),
applyStyleMode: options => runtimeCore?.applyStyleMode(options),
confirmAction: options => confirmAction(options),
setStatus,
showAppToast,
Expand Down Expand Up @@ -1325,8 +1339,12 @@ const runtimeCoreOptions = createRuntimeCoreOptions({
getStyleEditorLanguage,
workspaceTabsState,
queueWorkspaceSave,
getRenderModeCompatibilityError: mode =>
normalizeRenderMode(mode) === 'react'
? getReactEntryTabCompatibilityError(getEntryWorkspaceTab())
: null,
})
const runtimeCore = createRuntimeCoreSetup(runtimeCoreOptions)
runtimeCore = createRuntimeCoreSetup(runtimeCoreOptions)

diagnosticsFlowController = runtimeCore.diagnosticsFlowController
renderRuntime = runtimeCore.renderRuntime
Expand All @@ -1349,36 +1367,8 @@ const maybeRender = () => diagnosticsFlowController.maybeRender()
const maybeRenderFromComponentEditorChange = () =>
diagnosticsFlowController.maybeRenderFromComponentEditorChange()

function setJsxSource(value) {
setJsxSourceValue({
value,
jsxCodeEditor,
setSuppressEditorChangeSideEffects: nextValue =>
(suppressEditorChangeSideEffects = nextValue),
jsxEditor,
})
}

function setCssSource(value) {
setCssSourceValue({
value,
cssCodeEditor,
setSuppressEditorChangeSideEffects: nextValue =>
(suppressEditorChangeSideEffects = nextValue),
cssEditor,
})
}

const confirmAction = options => runtimeCore.confirmAction(options)

function applyRenderMode({ mode, fromActivePrContext: _fromActivePrContext = false }) {
runtimeCore.applyRenderMode({ mode, fromActivePrContext: _fromActivePrContext })
}

function applyStyleMode({ mode }) {
runtimeCore.applyStyleMode({ mode })
}

bindAppEventsAndStart({
editorUi: {
renderMode,
Expand All @@ -1405,8 +1395,8 @@ bindAppEventsAndStart({
statusNode,
},
sourceActions: {
applyRenderMode,
applyStyleMode,
applyRenderMode: options => runtimeCore.applyRenderMode(options),
applyStyleMode: options => runtimeCore.applyStyleMode(options),
updateRenderButtonVisibility: () => (renderButton.hidden = autoRenderToggle.checked),
clearDiagnosticsScope,
clearComponentLintDiagnosticsState,
Expand Down
3 changes: 3 additions & 0 deletions src/modules/app-core/app-composition-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const createRuntimeCoreOptions = ({
clearConfirmCopy,
clearConfirmButton,
setPendingClearAction,
getRenderModeCompatibilityError,
normalizeRenderMode,
normalizeStyleMode,
resetDiagnosticsFlow,
Expand Down Expand Up @@ -108,6 +109,8 @@ const createRuntimeCoreOptions = ({
clearConfirmCopy,
clearConfirmButton,
setPendingClearAction,
setStatus,
getRenderModeCompatibilityError,
normalizeRenderMode,
normalizeStyleMode,
resetDiagnosticsFlow,
Expand Down
7 changes: 7 additions & 0 deletions src/modules/app-core/runtime-core-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ const createRuntimeCoreSetup = ({
clearConfirmCopy,
clearConfirmButton,
setPendingClearAction,
setStatus,
normalizeRenderMode,
normalizeStyleMode,
getRenderModeCompatibilityError = () => null,
resetDiagnosticsFlow,
maybeRender,
flushWorkspaceSave,
Expand Down Expand Up @@ -101,6 +103,11 @@ const createRuntimeCoreSetup = ({
queueWorkspaceSave()
resetDiagnosticsFlow()

const compatibilityError = getRenderModeCompatibilityError(nextMode)
if (compatibilityError instanceof Error) {
setStatus(compatibilityError.message, 'error')
}

maybeRender()
void flushWorkspaceSave().catch(() => {
/* Save failures are already surfaced through saver onError. */
Expand Down
34 changes: 34 additions & 0 deletions src/modules/app-core/source-setters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const createSourceSetters = ({
setJsxSourceValue,
setCssSourceValue,
getJsxCodeEditor,
getCssCodeEditor,
setSuppressEditorChangeSideEffects,
jsxEditor,
cssEditor,
}) => {
const setJsxSource = value => {
setJsxSourceValue({
value,
jsxCodeEditor: getJsxCodeEditor(),
setSuppressEditorChangeSideEffects,
jsxEditor,
})
}

const setCssSource = value => {
setCssSourceValue({
value,
cssCodeEditor: getCssCodeEditor(),
setSuppressEditorChangeSideEffects,
cssEditor,
})
}

return {
setJsxSource,
setCssSource,
}
}

export { createSourceSetters }
6 changes: 4 additions & 2 deletions src/modules/app-core/workspace-context-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ const createWorkspaceContextController = ({
setWorkspaceScopeMarker(nextScope)
}

const nextTabs = ensureWorkspaceTabsShape(workspace.tabs)
const nextRenderMode = normalizeRenderMode(workspace.renderMode)
const nextTabs = ensureWorkspaceTabsShape(workspace.tabs, {
renderMode: nextRenderMode,
})
if (typeof workspace.base === 'string' && githubPrBaseBranch) {
githubPrBaseBranch.value = workspace.base
}
Expand All @@ -124,7 +127,6 @@ const createWorkspaceContextController = ({
}),
})

const nextRenderMode = normalizeRenderMode(workspace.renderMode)
if (getRenderModeValue() !== nextRenderMode) {
setRenderModeValue(nextRenderMode)
}
Expand Down
5 changes: 3 additions & 2 deletions src/modules/app-core/workspace-controllers-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const createWorkspaceControllersSetup = ({
workspaceTabsShell,
workspaceTabAddWrap,
setWorkspaceTabRenameState,
allowedEntryTabFileNames,
getAllowedEntryTabFileNames,
getPathFileName,
normalizeEntryTabPath,
normalizeModuleTabPathForRename,
Expand Down Expand Up @@ -135,7 +135,8 @@ const createWorkspaceControllersSetup = ({
},
renderWorkspaceTabs: () => renderWorkspaceTabs(),
setStatus,
allowedEntryTabFileNames,
getAllowedEntryTabFileNames,
getRenderModeValue,
getPathFileName,
normalizeEntryTabPath,
normalizeModuleTabPathForRename,
Expand Down
45 changes: 43 additions & 2 deletions src/modules/app-core/workspace-tab-mutations-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const createWorkspaceTabMutationsController = ({
setWorkspaceTabRenameState,
renderWorkspaceTabs,
setStatus,
allowedEntryTabFileNames,
getAllowedEntryTabFileNames,
getRenderModeValue,
getPathFileName,
normalizeEntryTabPath,
normalizeModuleTabPathForRename,
Expand All @@ -27,6 +28,41 @@ const createWorkspaceTabMutationsController = ({
createWorkspaceTabId,
getShouldShowEditedDesign,
}) => {
const defaultAllowedEntryTabFileNames = new Set(['app.tsx', 'app.js'])

const formatAllowedEntryTabNames = allowedEntryTabFileNames => {
const displayNames = [...allowedEntryTabFileNames].map(fileName =>
fileName.toLowerCase().startsWith('app.')
? `App.${fileName.slice('app.'.length)}`
: fileName,
)

if (displayNames.length <= 1) {
return displayNames[0] || 'App.tsx'
}

if (displayNames.length === 2) {
return `${displayNames[0]} or ${displayNames[1]}`
}

const leading = displayNames.slice(0, -1).join(', ')
return `${leading}, or ${displayNames[displayNames.length - 1]}`
}

const resolveAllowedEntryTabFileNames = () => {
if (typeof getAllowedEntryTabFileNames !== 'function') {
return defaultAllowedEntryTabFileNames
}

const resolved = getAllowedEntryTabFileNames({
renderMode:
typeof getRenderModeValue === 'function' ? getRenderModeValue() : undefined,
})
return resolved instanceof Set && resolved.size > 0
? resolved
: defaultAllowedEntryTabFileNames
}

const moduleTabTemplates = {
script: {
basePath: 'src/components/module.tsx',
Expand Down Expand Up @@ -87,12 +123,16 @@ const createWorkspaceTabMutationsController = ({

const includesDirectory = /[\\/]/.test(normalizedNameInput)
const nextFileName = getPathFileName(normalizedNameInput) || normalizedNameInput
const allowedEntryTabFileNames = resolveAllowedEntryTabFileNames()

if (
tab.role === 'entry' &&
!allowedEntryTabFileNames.has(nextFileName.toLowerCase())
) {
setStatus('Entry tab name must be App.tsx or App.js.', 'error')
setStatus(
`Entry tab name must be ${formatAllowedEntryTabNames(allowedEntryTabFileNames)}.`,
'error',
)
renderWorkspaceTabs()
return
}
Expand All @@ -103,6 +143,7 @@ const createWorkspaceTabMutationsController = ({
preferredFileName: includesDirectory
? getPathFileName(normalizedNameInput)
: normalizedNameInput,
allowedEntryTabFileNames,
})
: normalizeModuleTabPathForRename(tab.path, normalizedNameInput)

Expand Down
Loading
Loading