From 9791ccfb52bb5bde997c3c60c56df119c8bd7c64 Mon Sep 17 00:00:00 2001 From: KCM Date: Fri, 1 May 2026 13:37:57 -0500 Subject: [PATCH 1/2] test: more resilient assertion. --- playwright/github-byot-ai.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/github-byot-ai.spec.ts b/playwright/github-byot-ai.spec.ts index add40b3..b3413b3 100644 --- a/playwright/github-byot-ai.spec.ts +++ b/playwright/github-byot-ai.spec.ts @@ -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, }, From 3439b09f0d7f0742c51525aacba7d6798b58abcb Mon Sep 17 00:00:00 2001 From: KCM Date: Fri, 1 May 2026 14:28:43 -0500 Subject: [PATCH 2/2] feat: mode aware entry tab extensions. --- AGENTS.md | 1 + playwright/rendering-modes/core.spec.ts | 31 ++++++++++ src/app.js | 60 ++++++++----------- .../app-core/app-composition-options.js | 3 + src/modules/app-core/runtime-core-setup.js | 7 +++ src/modules/app-core/source-setters.js | 34 +++++++++++ .../app-core/workspace-context-controller.js | 6 +- .../app-core/workspace-controllers-setup.js | 5 +- .../workspace-tab-mutations-controller.js | 45 +++++++++++++- src/modules/diagnostics/type-diagnostics.js | 5 +- src/modules/preview/preview-entry-resolver.js | 17 ++++++ src/modules/preview/render-runtime.js | 18 +++++- .../workspace/workspace-tab-helpers.js | 12 +++- src/modules/workspace/workspace-tab-shape.js | 8 ++- 14 files changed, 206 insertions(+), 46 deletions(-) create mode 100644 src/modules/app-core/source-setters.js diff --git a/AGENTS.md b/AGENTS.md index 7c3c324..c033f05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/playwright/rendering-modes/core.spec.ts b/playwright/rendering-modes/core.spec.ts index e838f8a..8724a65 100644 --- a/playwright/rendering-modes/core.spec.ts +++ b/playwright/rendering-modes/core.spec.ts @@ -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) diff --git a/src/app.js b/src/app.js index 4361cd9..529bec8 100644 --- a/src/app.js +++ b/src/app.js @@ -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, @@ -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' @@ -80,6 +82,7 @@ import { createEnsureWorkspaceTabsShape } from './modules/workspace/workspace-ta import { createWorkspaceRecordId, getDirtyStateForTabChange, + getAllowedEntryTabFileNames, getPathFileName, getTabKind, getTabTargetPrFilePath, @@ -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, @@ -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 @@ -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)) { @@ -695,6 +708,7 @@ const ensureWorkspaceTabsShape = createEnsureWorkspaceTabsShape({ defaultStylesTabPath, defaultJsx, normalizeEntryTabPath, + getAllowedEntryTabFileNames, getPathFileName, getTabTargetPrFilePath, normalizeWorkspacePathValue, @@ -791,7 +805,7 @@ const { workspaceTabsShell, workspaceTabAddWrap, setWorkspaceTabRenameState: value => (workspaceTabRenameState = value), - allowedEntryTabFileNames, + getAllowedEntryTabFileNames, getPathFileName, normalizeEntryTabPath, normalizeModuleTabPathForRename, @@ -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, @@ -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 @@ -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, @@ -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, diff --git a/src/modules/app-core/app-composition-options.js b/src/modules/app-core/app-composition-options.js index 5b163d9..6ea4791 100644 --- a/src/modules/app-core/app-composition-options.js +++ b/src/modules/app-core/app-composition-options.js @@ -42,6 +42,7 @@ const createRuntimeCoreOptions = ({ clearConfirmCopy, clearConfirmButton, setPendingClearAction, + getRenderModeCompatibilityError, normalizeRenderMode, normalizeStyleMode, resetDiagnosticsFlow, @@ -108,6 +109,8 @@ const createRuntimeCoreOptions = ({ clearConfirmCopy, clearConfirmButton, setPendingClearAction, + setStatus, + getRenderModeCompatibilityError, normalizeRenderMode, normalizeStyleMode, resetDiagnosticsFlow, diff --git a/src/modules/app-core/runtime-core-setup.js b/src/modules/app-core/runtime-core-setup.js index 6f8d1e7..a75db57 100644 --- a/src/modules/app-core/runtime-core-setup.js +++ b/src/modules/app-core/runtime-core-setup.js @@ -8,8 +8,10 @@ const createRuntimeCoreSetup = ({ clearConfirmCopy, clearConfirmButton, setPendingClearAction, + setStatus, normalizeRenderMode, normalizeStyleMode, + getRenderModeCompatibilityError = () => null, resetDiagnosticsFlow, maybeRender, flushWorkspaceSave, @@ -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. */ diff --git a/src/modules/app-core/source-setters.js b/src/modules/app-core/source-setters.js new file mode 100644 index 0000000..f7490ef --- /dev/null +++ b/src/modules/app-core/source-setters.js @@ -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 } diff --git a/src/modules/app-core/workspace-context-controller.js b/src/modules/app-core/workspace-context-controller.js index f08c056..9c9bd27 100644 --- a/src/modules/app-core/workspace-context-controller.js +++ b/src/modules/app-core/workspace-context-controller.js @@ -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 } @@ -124,7 +127,6 @@ const createWorkspaceContextController = ({ }), }) - const nextRenderMode = normalizeRenderMode(workspace.renderMode) if (getRenderModeValue() !== nextRenderMode) { setRenderModeValue(nextRenderMode) } diff --git a/src/modules/app-core/workspace-controllers-setup.js b/src/modules/app-core/workspace-controllers-setup.js index e40eb07..60b3475 100644 --- a/src/modules/app-core/workspace-controllers-setup.js +++ b/src/modules/app-core/workspace-controllers-setup.js @@ -56,7 +56,7 @@ const createWorkspaceControllersSetup = ({ workspaceTabsShell, workspaceTabAddWrap, setWorkspaceTabRenameState, - allowedEntryTabFileNames, + getAllowedEntryTabFileNames, getPathFileName, normalizeEntryTabPath, normalizeModuleTabPathForRename, @@ -135,7 +135,8 @@ const createWorkspaceControllersSetup = ({ }, renderWorkspaceTabs: () => renderWorkspaceTabs(), setStatus, - allowedEntryTabFileNames, + getAllowedEntryTabFileNames, + getRenderModeValue, getPathFileName, normalizeEntryTabPath, normalizeModuleTabPathForRename, diff --git a/src/modules/app-core/workspace-tab-mutations-controller.js b/src/modules/app-core/workspace-tab-mutations-controller.js index b5c5694..7a1a10b 100644 --- a/src/modules/app-core/workspace-tab-mutations-controller.js +++ b/src/modules/app-core/workspace-tab-mutations-controller.js @@ -4,7 +4,8 @@ const createWorkspaceTabMutationsController = ({ setWorkspaceTabRenameState, renderWorkspaceTabs, setStatus, - allowedEntryTabFileNames, + getAllowedEntryTabFileNames, + getRenderModeValue, getPathFileName, normalizeEntryTabPath, normalizeModuleTabPathForRename, @@ -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', @@ -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 } @@ -103,6 +143,7 @@ const createWorkspaceTabMutationsController = ({ preferredFileName: includesDirectory ? getPathFileName(normalizedNameInput) : normalizedNameInput, + allowedEntryTabFileNames, }) : normalizeModuleTabPathForRename(tab.path, normalizedNameInput) diff --git a/src/modules/diagnostics/type-diagnostics.js b/src/modules/diagnostics/type-diagnostics.js index f73a67e..cec924a 100644 --- a/src/modules/diagnostics/type-diagnostics.js +++ b/src/modules/diagnostics/type-diagnostics.js @@ -837,7 +837,10 @@ export const createTypeDiagnosticsController = ({ : '' const sourceFileName = normalizedSourcePathOverride || resolvedEntryTab?.path || 'component.tsx' - const typecheckSourceFileName = sourceFileName.replace(/\.(jsx?|mjs|cjs)$/i, '.tsx') + const typecheckSourceFileName = sourceFileName.replace( + /\.(jsx?|tsx?|mjs|cjs|mts|cts)$/i, + '.tsx', + ) const jsxTypesFileName = 'knighted-jsx-runtime.d.ts' const styleImportTypesFileName = 'knighted-style-imports.d.ts' const renderMode = getRenderMode() diff --git a/src/modules/preview/preview-entry-resolver.js b/src/modules/preview/preview-entry-resolver.js index 9b7514d..dbf6842 100644 --- a/src/modules/preview/preview-entry-resolver.js +++ b/src/modules/preview/preview-entry-resolver.js @@ -1,4 +1,8 @@ const previewEntryNamePattern = /(?:^|\/)(?:app|main)\.[jt]sx?$/i +const reactCompatibleEntryNamePattern = /(?:^|\/)[^/]+\.(?:tsx|jsx|js)$/i +const reactEntryTabCompatibilityErrorName = 'ReactEntryTabCompatibilityError' +const reactEntryTabCompatibilityErrorMessage = + 'React mode requires the entry tab to end in .tsx, .jsx, or .js.' const normalizeTabIdentity = tab => { if (!tab || typeof tab !== 'object') { @@ -42,3 +46,16 @@ export const canRenderPreview = ({ tabs, fallbackSource = '' } = {}) => { return typeof fallbackSource === 'string' && fallbackSource.trim().length > 0 } + +export const getReactEntryTabCompatibilityError = tab => { + const identity = normalizeTabIdentity(tab) + if (!identity || reactCompatibleEntryNamePattern.test(identity)) { + return null + } + + const error = new Error(reactEntryTabCompatibilityErrorMessage) + error.name = reactEntryTabCompatibilityErrorName + return error +} + +export { reactEntryTabCompatibilityErrorMessage, reactEntryTabCompatibilityErrorName } diff --git a/src/modules/preview/render-runtime.js b/src/modules/preview/render-runtime.js index 93f5a43..fef9440 100644 --- a/src/modules/preview/render-runtime.js +++ b/src/modules/preview/render-runtime.js @@ -1,4 +1,9 @@ -import { canRenderPreview, resolvePreviewEntryTab } from './preview-entry-resolver.js' +import { + canRenderPreview, + getReactEntryTabCompatibilityError, + reactEntryTabCompatibilityErrorName, + resolvePreviewEntryTab, +} from './preview-entry-resolver.js' import { createWorkspaceIframePreviewBridge } from '../preview-runtime/iframe-preview-executor.js' import { planWorkspaceVirtualModules } from '../preview-runtime/virtual-workspace-modules.js' import { createPreviewWorkspaceGraphCache } from './preview-workspace-graph.js' @@ -670,7 +675,9 @@ export const createRenderRuntimeController = ({ const renderPreviewError = error => { disposeWorkspaceModules() disposeIframeBridge() - setStatus('Error', 'error') + const shouldSurfaceSpecificStatus = + error instanceof Error && error.name === reactEntryTabCompatibilityErrorName + setStatus(shouldSurfaceSpecificStatus ? error.message : 'Error', 'error') const target = getRenderTarget() clearTarget(target) @@ -709,6 +716,13 @@ export const createRenderRuntimeController = ({ throw new Error('Unable to resolve workspace preview entry tab.') } + if (mode === 'react') { + const compatibilityError = getReactEntryTabCompatibilityError(entryTab) + if (compatibilityError) { + throw compatibilityError + } + } + const { transformJsxSource } = await ensureCoreRuntime() const tabsForExecution = workspaceTabs const entryTabForExecution = diff --git a/src/modules/workspace/workspace-tab-helpers.js b/src/modules/workspace/workspace-tab-helpers.js index d0b2829..106fa26 100644 --- a/src/modules/workspace/workspace-tab-helpers.js +++ b/src/modules/workspace/workspace-tab-helpers.js @@ -2,7 +2,16 @@ const defaultStyleTabLanguages = new Set(['css', 'less', 'sass', 'module']) const defaultComponentTabPath = 'src/components/App.tsx' const defaultComponentTabName = 'App.tsx' const defaultEntryTabDirectory = 'src/components' -const defaultAllowedEntryTabFileNames = new Set(['app.tsx', 'app.js']) +const domAllowedEntryTabFileNames = new Set(['app.ts', 'app.tsx', 'app.js']) +const reactAllowedEntryTabFileNames = new Set(['app.tsx', 'app.jsx', 'app.js']) +const defaultAllowedEntryTabFileNames = domAllowedEntryTabFileNames + +const getAllowedEntryTabFileNames = ({ renderMode } = {}) => { + const normalizedMode = toNonEmptyWorkspaceText(renderMode).toLowerCase() + return normalizedMode === 'react' + ? reactAllowedEntryTabFileNames + : domAllowedEntryTabFileNames +} const toNonEmptyWorkspaceText = value => typeof value === 'string' && value.trim().length > 0 ? value.trim() : '' @@ -250,6 +259,7 @@ export { createWorkspaceRecordId, defaultStyleTabLanguages, getDirtyStateForTabChange, + getAllowedEntryTabFileNames, getPathDirectory, getPathFileName, getTabKind, diff --git a/src/modules/workspace/workspace-tab-shape.js b/src/modules/workspace/workspace-tab-shape.js index 6b41c5b..9c77d87 100644 --- a/src/modules/workspace/workspace-tab-shape.js +++ b/src/modules/workspace/workspace-tab-shape.js @@ -4,6 +4,7 @@ const createEnsureWorkspaceTabsShape = defaultComponentTabPath, defaultJsx, normalizeEntryTabPath, + getAllowedEntryTabFileNames, getPathFileName, getTabTargetPrFilePath, normalizeWorkspacePathValue, @@ -13,10 +14,14 @@ const createEnsureWorkspaceTabsShape = toNonEmptyWorkspaceText, isStyleTabLanguage, }) => - tabs => { + (tabs, { renderMode } = {}) => { const inputTabs = Array.isArray(tabs) ? tabs : [] const hasEntryTab = inputTabs.some(tab => tab?.role === 'entry') const nextTabs = [...inputTabs] + const allowedEntryTabFileNames = + typeof getAllowedEntryTabFileNames === 'function' + ? getAllowedEntryTabFileNames({ renderMode }) + : undefined if (!hasEntryTab) { nextTabs.unshift({ @@ -34,6 +39,7 @@ const createEnsureWorkspaceTabsShape = if (tab?.role === 'entry') { const normalizedEntryPath = normalizeEntryTabPath(tab.path, { preferredFileName: tab.name, + allowedEntryTabFileNames, }) const normalizedEntryTargetPath = normalizeWorkspacePathValue(normalizedEntryPath) return {