diff --git a/playwright/github-pr-drawer/active-context-switch.spec.ts b/playwright/github-pr-drawer/active-context-switch.spec.ts index 5923a0b..333ff38 100644 --- a/playwright/github-pr-drawer/active-context-switch.spec.ts +++ b/playwright/github-pr-drawer/active-context-switch.spec.ts @@ -1719,10 +1719,13 @@ test('Active PR context push with no local changes shows neutral status', async const tabs = Array.isArray(workspaceRecord?.tabs) ? (workspaceRecord.tabs as Array>) : [] - const tabIds = new Set( - tabs.map(tab => (typeof tab?.id === 'string' ? tab.id : '')).filter(Boolean), - ) - const hasPrimaryTabs = tabIds.has('component') && tabIds.has('styles') + const hasEntryTab = tabs.some(tab => tab?.role === 'entry') + const hasStyleTab = tabs.some(tab => { + const language = + typeof tab?.language === 'string' ? tab.language.trim().toLowerCase() : '' + return language === 'css' || language === 'less' || language === 'sass' + }) + const hasPrimaryTabs = hasEntryTab && hasStyleTab return hasPrimaryTabs && tabs.every(tab => tab?.isDirty === false) }) .toBe(true) diff --git a/playwright/github-pr-drawer/active-context-sync.spec.ts b/playwright/github-pr-drawer/active-context-sync.spec.ts index 4a79865..87f4b4f 100644 --- a/playwright/github-pr-drawer/active-context-sync.spec.ts +++ b/playwright/github-pr-drawer/active-context-sync.spec.ts @@ -1045,10 +1045,13 @@ test('Active PR context push commit uses Git Database API atomic path by default const tabs = Array.isArray(workspaceRecord?.tabs) ? (workspaceRecord.tabs as Array>) : [] - const tabIds = new Set( - tabs.map(tab => (typeof tab?.id === 'string' ? tab.id : '')).filter(Boolean), - ) - const hasPrimaryTabs = tabIds.has('component') && tabIds.has('styles') + const hasEntryTab = tabs.some(tab => tab?.role === 'entry') + const hasStyleTab = tabs.some(tab => { + const language = + typeof tab?.language === 'string' ? tab.language.trim().toLowerCase() : '' + return language === 'css' || language === 'less' || language === 'sass' + }) + const hasPrimaryTabs = hasEntryTab && hasStyleTab return hasPrimaryTabs && tabs.every(tab => tab?.isDirty === false) }, { timeout: 10_000 }, @@ -2186,8 +2189,11 @@ test('Reloaded active PR context does not apply partial sync when one primary fi ? (workspaceRecord.tabs as Array>) : [] - const entryTab = tabs.find(tab => tab?.id === 'component') - const stylesTab = tabs.find(tab => tab?.id === 'styles') + const entryTab = tabs.find(tab => tab?.role === 'entry') + const stylesTab = tabs.find( + tab => + typeof tab?.path === 'string' && tab.path.trim() === 'src/styles/app.css', + ) return { entryContent: typeof entryTab?.content === 'string' ? entryTab.content : '', @@ -2377,7 +2383,7 @@ test('Reloaded active PR context sync does not overwrite non-primary module tabs ? (workspaceRecord.tabs as Array>) : [] - const entryTab = tabs.find(tab => tab?.id === 'component') + const entryTab = tabs.find(tab => tab?.role === 'entry') const boopTab = tabs.find(tab => tab?.id === 'module-boop') const beepTab = tabs.find(tab => tab?.id === 'module-beep') @@ -2590,7 +2596,7 @@ test('Reloaded active PR context sync does not overwrite non-primary tabs with s ? (workspaceRecord.tabs as Array>) : [] - const entryTab = tabs.find(tab => tab?.id === 'component') + const entryTab = tabs.find(tab => tab?.role === 'entry') const boopTab = tabs.find(tab => tab?.id === 'module-boop') const beepTab = tabs.find(tab => tab?.id === 'module-beep') diff --git a/playwright/github-pr-drawer/github-pr-drawer.helpers.ts b/playwright/github-pr-drawer/github-pr-drawer.helpers.ts index dd0782c..721a4fa 100644 --- a/playwright/github-pr-drawer/github-pr-drawer.helpers.ts +++ b/playwright/github-pr-drawer/github-pr-drawer.helpers.ts @@ -566,7 +566,7 @@ export const seedActivePrWorkspaceContext = async ( renderMode, tabs: [ { - id: 'component', + id: 'entry', name: 'App.tsx', path: 'src/components/App.tsx', language: 'javascript-jsx', @@ -575,7 +575,7 @@ export const seedActivePrWorkspaceContext = async ( content: 'export const App = () =>
Hello from Knighted
', }, { - id: 'styles', + id: 'style', name: 'app.css', path: 'src/styles/app.css', language: safeStyleLanguage, @@ -584,7 +584,7 @@ export const seedActivePrWorkspaceContext = async ( content: 'main { color: #111; }', }, ], - activeTabId: 'component', + activeTabId: 'entry', createdAt: Date.now() + 60_000, lastModified: Date.now() + 60_000, }, @@ -706,7 +706,7 @@ export const getWorkspaceComponentContent = (record: Record | n return false } - return (tab as { id?: unknown }).id === 'component' + return (tab as { role?: unknown }).role === 'entry' }) as { content?: unknown } | undefined return typeof componentTab?.content === 'string' ? componentTab.content : '' @@ -829,7 +829,7 @@ export const runActiveWorkspaceSwitchIntegrityScenario = async ({ renderMode: 'react', tabs: [ { - id: 'component', + id: 'entry', name: 'App.tsx', path: 'src/components/App.tsx', language: 'javascript-jsx', @@ -838,7 +838,7 @@ export const runActiveWorkspaceSwitchIntegrityScenario = async ({ content: 'export const App = () =>
Active A content
', }, ], - activeTabId: 'component', + activeTabId: 'entry', createdAt: Date.now() - 60_000, lastModified: Date.now() - 60_000, }, @@ -853,7 +853,7 @@ export const runActiveWorkspaceSwitchIntegrityScenario = async ({ renderMode: 'dom', tabs: [ { - id: 'component', + id: 'entry', name: 'App.tsx', path: 'src/components/App.tsx', language: 'javascript-jsx', @@ -862,7 +862,7 @@ export const runActiveWorkspaceSwitchIntegrityScenario = async ({ content: `export const App = () =>
Target ${targetState} content
`, }, ], - activeTabId: 'component', + activeTabId: 'entry', createdAt: Date.now() - 120_000, lastModified: Date.now() - 120_000, }, @@ -1077,7 +1077,7 @@ export const runActiveWorkspaceCrossRepoSwitchIntegrityScenario = async ({ renderMode: 'react', tabs: [ { - id: 'component', + id: 'entry', name: 'App.tsx', path: 'src/components/App.tsx', language: 'javascript-jsx', @@ -1086,7 +1086,7 @@ export const runActiveWorkspaceCrossRepoSwitchIntegrityScenario = async ({ content: 'export const App = () =>
Cross source active content
', }, ], - activeTabId: 'component', + activeTabId: 'entry', createdAt: Date.now() - 60_000, lastModified: Date.now() - 60_000, }, @@ -1101,7 +1101,7 @@ export const runActiveWorkspaceCrossRepoSwitchIntegrityScenario = async ({ renderMode: 'dom', tabs: [ { - id: 'component', + id: 'entry', name: 'App.tsx', path: 'src/components/App.tsx', language: 'javascript-jsx', @@ -1110,7 +1110,7 @@ export const runActiveWorkspaceCrossRepoSwitchIntegrityScenario = async ({ content: `export const App = () =>
Cross target ${targetState} content
`, }, ], - activeTabId: 'component', + activeTabId: 'entry', createdAt: Date.now() - 120_000, lastModified: Date.now() - 120_000, }, diff --git a/playwright/github-pr-drawer/open-pr-create.spec.ts b/playwright/github-pr-drawer/open-pr-create.spec.ts index 6d38106..d044033 100644 --- a/playwright/github-pr-drawer/open-pr-create.spec.ts +++ b/playwright/github-pr-drawer/open-pr-create.spec.ts @@ -784,7 +784,7 @@ test('Open PR success normalizes trailing newline without showing Edited indicat await setComponentEditorSource(page, 'const App = () => ') await setStylesEditorSource(page, '.button { color: red; }') - await addWorkspaceTab(page, { kind: 'styles' }) + await addWorkspaceTab(page, { type: 'style' }) const moduleStylesEditor = page .locator('.editor-panel[data-editor-kind="styles"] .cm-content') @@ -816,7 +816,7 @@ test('Open PR success normalizes trailing newline without showing Edited indicat ? (workspaceRecord.tabs as Array>) : [] - const componentTab = tabs.find(tab => tab?.id === 'component') + const componentTab = tabs.find(tab => tab?.role === 'entry') const appStylesTab = tabs.find( tab => typeof tab?.path === 'string' && tab.path.trim() === 'src/styles/app.css', diff --git a/playwright/helpers/app-test-helpers.ts b/playwright/helpers/app-test-helpers.ts index 1bf1d7c..20c2f08 100644 --- a/playwright/helpers/app-test-helpers.ts +++ b/playwright/helpers/app-test-helpers.ts @@ -83,7 +83,8 @@ export const waitForAppReady = async (page: Page, path = appEntryPath) => { return ( statusText === 'Rendered' || statusText?.startsWith('Rendered (Type errors:') || - statusText === 'Error' + statusText === 'Error' || + statusText === 'Could not restore local workspace context.' ) }) .toBe(true) @@ -155,10 +156,10 @@ export const getPreviewFrame = (page: Page) => page.frameLocator('#preview-host export const addWorkspaceTab = async ( page: Page, - { kind = 'component' }: { kind?: 'component' | 'styles' } = {}, + { type = 'script' }: { type?: 'script' | 'style' } = {}, ) => { await page.getByRole('button', { name: 'Add workspace tab' }).click() - if (kind === 'styles') { + if (type === 'style') { await page.getByRole('button', { name: 'Add styles tab' }).click() return } diff --git a/playwright/rendering-modes/core.spec.ts b/playwright/rendering-modes/core.spec.ts index f9caf1b..e838f8a 100644 --- a/playwright/rendering-modes/core.spec.ts +++ b/playwright/rendering-modes/core.spec.ts @@ -13,6 +13,127 @@ import { waitForInitialRender, } from '../helpers/app-test-helpers.js' +const renameWorkspaceTab = async ( + page: import('@playwright/test').Page, + { + from, + to, + }: { + from: string + to: string + }, +) => { + await page.getByRole('button', { name: `Rename tab ${from}` }).click() + const renameInput = page.getByLabel(`Rename ${from}`) + await renameInput.fill(to) + await renameInput.press('Enter') +} + +const renameWorkspaceTabFromCandidates = async ( + page: import('@playwright/test').Page, + { + fromCandidates, + to, + }: { + fromCandidates: string[] + to: string + }, +) => { + for (const from of fromCandidates) { + const button = page.getByRole('button', { name: `Rename tab ${from}` }) + if ((await button.count()) === 0) { + continue + } + + await button.click() + const renameInput = page.getByLabel(`Rename ${from}`) + await renameInput.fill(to) + await renameInput.press('Enter') + return + } + + throw new Error( + `Could not find a rename target from candidates: ${fromCandidates.join(', ')}`, + ) +} + +const readLatestWorkspaceSnapshot = async (page: import('@playwright/test').Page) => { + return page.evaluate(async () => { + const dbName = 'knighted-develop-workspaces' + const storeName = 'prWorkspaces' + + const openDb = await new Promise(resolve => { + try { + const request = indexedDB.open(dbName) + request.onsuccess = () => resolve(request.result) + request.onerror = () => resolve(null) + } catch { + resolve(null) + } + }) + + if (!openDb) { + return null + } + + const records = await new Promise>>(resolve => { + try { + const transaction = openDb.transaction(storeName, 'readonly') + const store = transaction.objectStore(storeName) + const request = store.getAll() + request.onsuccess = () => { + const value = Array.isArray(request.result) ? request.result : [] + resolve(value as Array>) + } + request.onerror = () => resolve([]) + } catch { + resolve([]) + } + }) + + openDb.close() + + if (!Array.isArray(records) || records.length === 0) { + return null + } + + const sorted = [...records].sort((a, b) => { + const first = + typeof a.lastModified === 'number' && Number.isFinite(a.lastModified) + ? a.lastModified + : 0 + const second = + typeof b.lastModified === 'number' && Number.isFinite(b.lastModified) + ? b.lastModified + : 0 + return second - first + }) + + const latest = sorted[0] ?? {} + const tabs = Array.isArray(latest.tabs) ? latest.tabs : [] + const primaryStylesTab = tabs.find(tab => { + if (!tab || typeof tab !== 'object') { + return false + } + + const tabRecord = tab as Record + const language = typeof tabRecord.language === 'string' ? tabRecord.language : '' + const path = typeof tabRecord.path === 'string' ? tabRecord.path : '' + const name = typeof tabRecord.name === 'string' ? tabRecord.name : '' + const isStyleLanguage = ['css', 'less', 'sass', 'module'].includes(language) + const styleIdentity = `${path} ${name}`.toLowerCase() + const looksLikeStyle = /\.(css|less|sass|scss)\b/.test(styleIdentity) + return isStyleLanguage && looksLikeStyle + }) as Record | undefined + + return { + renderMode: typeof latest.renderMode === 'string' ? latest.renderMode : '', + styleLanguage: + typeof primaryStylesTab?.language === 'string' ? primaryStylesTab.language : '', + } + }) +} + test.beforeEach(async ({ page }) => { await resetWorkbenchStorage(page) }) @@ -31,6 +152,95 @@ test('renders in react mode with css modules', async ({ page }) => { await expectPreviewHasRenderedContent(page) }) +test('css module imports expose class map for module tabs', async ({ page }) => { + await waitForInitialRender(page) + + await ensurePanelToolsVisible(page, 'component') + + await renameWorkspaceTab(page, { + from: 'app.css', + to: 'app.module.css', + }) + + await setWorkspaceTabSource(page, { + fileName: 'app.module.css', + kind: 'styles', + source: [ + '.list {', + ' display: grid;', + '}', + '', + '.item {', + ' color: rgb(10, 20, 30);', + '}', + ].join('\n'), + }) + + await addWorkspaceTab(page, { type: 'script' }) + await renameWorkspaceTab(page, { + from: 'module.tsx', + to: 'list.tsx', + }) + await setWorkspaceTabSource(page, { + fileName: 'list.tsx', + source: [ + "import styles from '../styles/app.module.css'", + "import { Item } from './item'", + '', + 'type ListProps = {', + ' items: string[]', + '}', + '', + 'export const List = ({ items }: ListProps) => (', + '
    ', + ' {items.map(item => (', + ' ', + ' ))}', + '
', + ')', + ].join('\n'), + }) + + await addWorkspaceTab(page, { type: 'script' }) + await renameWorkspaceTabFromCandidates(page, { + fromCandidates: ['module-2.tsx', 'module.tsx', 'module-1.tsx'], + to: 'item.tsx', + }) + await setWorkspaceTabSource(page, { + fileName: 'item.tsx', + source: [ + "import styles from '../styles/app.module.css'", + '', + 'type ItemProps = {', + ' value: string', + '}', + '', + 'export const Item = ({ value }: ItemProps) => (', + '
  • {value}
  • ', + ')', + ].join('\n'), + }) + + await setComponentEditorSource( + page, + [ + "import { List } from './list'", + '', + "const items = ['one', 'two']", + '', + 'const App = () => ', + ].join('\n'), + ) + + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') + await expect(page.locator('#preview-host pre')).toHaveCount(0) + await expect(getPreviewFrame(page).getByRole('listitem')).toHaveCount(2) + await expect(getPreviewFrame(page).getByRole('listitem').first()).toHaveCSS( + 'color', + 'rgb(10, 20, 30)', + ) +}) + test('preview styles require explicit import from entry graph', async ({ page }) => { await waitForInitialRender(page) @@ -73,8 +283,8 @@ test('preview styles require explicit import from entry graph', async ({ page }) test('nested module imports can bring styles into preview graph', async ({ page }) => { await waitForInitialRender(page) - await addWorkspaceTab(page, { kind: 'component' }) - await addWorkspaceTab(page, { kind: 'styles' }) + await addWorkspaceTab(page, { type: 'script' }) + await addWorkspaceTab(page, { type: 'style' }) await setWorkspaceTabSource(page, { fileName: 'module.tsx', @@ -384,6 +594,31 @@ test('editing-transient missing reference runtime errors are suppressed', async await expect(page.getByRole('status', { name: 'App status' })).not.toHaveText('Error') }) +test('missing component identifiers in App render as runtime errors', async ({ + page, +}) => { + await waitForInitialRender(page) + await ensurePanelToolsVisible(page, 'component') + await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react') + + await setComponentEditorSource( + page, + [ + 'const App = () => (', + ' ', + ' ', + ' ', + ')', + ].join('\n'), + ) + + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') + await expect(page.locator('#preview-host pre')).toContainText('[runtime]') + await expect(page.locator('#preview-host pre')).toContainText( + /List is not defined|Can't find variable:\s*List/, + ) +}) + test('preview iframe sandbox isolates parent origin access', async ({ page }) => { await waitForInitialRender(page) @@ -706,6 +941,10 @@ test('persists render mode across reload', async ({ page }) => { await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react') await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') + await expect + .poll(async () => (await readLatestWorkspaceSnapshot(page))?.renderMode ?? '') + .toBe('react') + await page.reload() await waitForInitialRender(page) await ensurePanelToolsVisible(page, 'component') @@ -721,6 +960,10 @@ test('persists style mode across reload', async ({ page }) => { await expect(page.locator('#style-mode')).toHaveValue('sass') await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') + await expect + .poll(async () => (await readLatestWorkspaceSnapshot(page))?.styleLanguage ?? '') + .toBe('sass') + await page.reload() await waitForInitialRender(page) diff --git a/playwright/workspace-tabs.spec.ts b/playwright/workspace-tabs.spec.ts index 408c4ae..4b73e3a 100644 --- a/playwright/workspace-tabs.spec.ts +++ b/playwright/workspace-tabs.spec.ts @@ -68,7 +68,7 @@ const seedSyncedComponentTab = async (page: import('@playwright/test').Page) => return tab } - if ((tab as { id?: unknown }).id !== 'component') { + if ((tab as { role?: unknown }).role !== 'entry') { return tab } @@ -572,7 +572,7 @@ test('add menu can create styles tab while component tab is active', async ({ pa await waitForInitialRender(page) await page.getByRole('button', { name: 'Open tab App.tsx' }).click() - await addWorkspaceTab(page, { kind: 'styles' }) + await addWorkspaceTab(page, { type: 'style' }) await expect(page.getByRole('button', { name: 'Open tab module.css' })).toHaveAttribute( 'aria-current', diff --git a/src/app.js b/src/app.js index c03608d..f5a69f8 100644 --- a/src/app.js +++ b/src/app.js @@ -14,11 +14,9 @@ import { import { createDiagnosticsFlowController } from './modules/app-core/diagnostics-flow-controller.js' import { createEditorBootstrapController } from './modules/app-core/editor-bootstrap-controller.js' import { - getInitialRenderMode as getInitialRenderModeValue, getStyleEditorLanguage, normalizeRenderMode, normalizeStyleMode, - persistRenderMode as persistRenderModeValue, setCssSourceValue, setJsxSourceValue, updateRenderModeEditability as updateRenderModeEditabilityValue, @@ -28,6 +26,7 @@ import { createWorkspaceContextSnapshotGetter, toStyleModeForTabLanguage, } from './modules/app-core/workspace-local-helpers.js' +import { createWorkspaceTabSelectors } from './modules/app-core/workspace-tab-selectors.js' import { createDiagnosticsTabStateHelpers } from './modules/app-core/diagnostics-tab-state-helpers.js' import { createWorkspaceEditorHelpers } from './modules/app-core/workspace-editor-helpers.js' import { createEditedIndicatorVisibilityController } from './modules/app-core/edited-indicator-visibility-controller.js' @@ -213,7 +212,6 @@ const defaultStylesTabPath = 'src/styles/app.css' const defaultComponentTabName = 'App.tsx' const defaultStylesTabName = 'app.css' const allowedEntryTabFileNames = new Set(['app.tsx', 'app.js']) -const renderModeStorageKey = 'knighted-develop:render-mode' const editorKinds = ['component', 'styles'] const editorPanelsByKind = { component: componentEditorPanel, @@ -254,7 +252,7 @@ let hasCompletedInitialWorkspaceBootstrap = false const workspaceTabsState = createWorkspaceTabsState({ tabs: [ { - id: 'component', + id: 'entry', name: defaultComponentTabName, path: defaultComponentTabPath, language: 'javascript-jsx', @@ -272,7 +270,7 @@ const workspaceTabsState = createWorkspaceTabsState({ content: defaultCss, }, ], - activeTabId: 'component', + activeTabId: 'entry', }) const editorPool = createEditorPoolManager({ maxMounted: 2 }) let workspaceTabRenameState = { @@ -589,20 +587,23 @@ const getWorkspaceContextSnapshot = createWorkspaceContextSnapshotGetter({ getPrNumber: () => workspacePrNumber, }) -let loadedComponentTabId = 'component' -let loadedStylesTabId = 'styles' - -const getActiveWorkspaceTab = () => - workspaceTabsState.getTab(workspaceTabsState.getActiveTabId()) +const { getActiveWorkspaceTab, getEntryWorkspaceTab, getPrimaryStyleWorkspaceTab } = + createWorkspaceTabSelectors({ + workspaceTabsState, + getTabKind, + toNonEmptyWorkspaceText, + }) +const isStyleWorkspaceTab = tab => isStyleTabLanguage(tab?.language) const { + clearTrackedWorkspaceTab, getWorkspaceTabByKind, syncHeaderLabels, persistActiveTabEditorContent, loadWorkspaceTabIntoEditor, } = createWorkspaceEditorHelpers({ workspaceTabsState, - getTabKind, + isStyleWorkspaceTab, editorKinds, editorPanelsByKind, editorHeaderLabelByKind, @@ -611,10 +612,8 @@ const { editedIndicatorVisibilityController.getShouldShowEditedDesign, defaultTabNameByKind, toNonEmptyWorkspaceText, - getLoadedStylesTabId: () => loadedStylesTabId, - getLoadedComponentTabId: () => loadedComponentTabId, - setLoadedStylesTabId: value => (loadedStylesTabId = value), - setLoadedComponentTabId: value => (loadedComponentTabId = value), + getEntryWorkspaceTab, + getPrimaryStyleWorkspaceTab, getCssSource: () => getCssSource(), getJsxSource: () => getJsxSource(), getDirtyStateForTabChange, @@ -630,7 +629,7 @@ const { const workspaceSyncController = createWorkspaceSyncController({ workspaceTabsState, - getTabKind, + isStyleWorkspaceTab, getTabTargetPrFilePath, normalizeWorkspacePathValue, toWorkspaceSyncedContent, @@ -640,9 +639,6 @@ const workspaceSyncController = createWorkspaceSyncController({ hasTabCommittedSyncState, getJsxSource: () => getJsxSource(), getCssSource: () => getCssSource(), - getWorkspaceTabByKind, - getLoadedComponentTabId: () => loadedComponentTabId, - getLoadedStylesTabId: () => loadedStylesTabId, queueWorkspaceSave: () => queueWorkspaceSave(), resolveWorkspaceRecordIdentity, getWorkspaceContextSnapshot, @@ -653,16 +649,8 @@ const workspaceSyncController = createWorkspaceSyncController({ normalizeRenderMode: mode => normalizeRenderMode(mode), }) -const getLoadedComponentWorkspaceTab = () => - workspaceTabsState.getTab(loadedComponentTabId) ?? getWorkspaceTabByKind('component') - -const getLoadedStylesWorkspaceTab = () => - workspaceTabsState.getTab(loadedStylesTabId) ?? getWorkspaceTabByKind('styles') - -const getTypecheckSourcePath = () => { - const loadedComponentTab = getLoadedComponentWorkspaceTab() - return toNonEmptyWorkspaceText(loadedComponentTab?.path) || defaultComponentTabPath -} +const getTypecheckSourcePath = () => + toNonEmptyWorkspaceText(getEntryWorkspaceTab()?.path) || defaultComponentTabPath const { clearDiagnosticsOnTabSwitch, @@ -671,9 +659,9 @@ const { syncDiagnosticsDrawerLayout, } = createDiagnosticsTabStateHelpers({ getActiveWorkspaceTab, - getLoadedComponentWorkspaceTab, - getLoadedStylesWorkspaceTab, - getTabKind, + getEntryWorkspaceTab, + getPrimaryStyleWorkspaceTab, + isStyleWorkspaceTab, toNonEmptyWorkspaceText, diagnosticsComponentSection, diagnosticsStylesSection, @@ -771,7 +759,6 @@ const { setRenderModeValue: value => { renderMode.value = value }, - persistRenderMode: mode => persistRenderMode(mode), getActiveWorkspaceTab, onActiveWorkspaceTabChange: (_tab, { changed } = {}) => { syncDiagnosticsDrawerLayout() @@ -815,11 +802,8 @@ const { workspaceTabAddMenuUi.setOpen(isOpen) }, confirmAction: options => confirmAction(options), - getTabKind, - getLoadedComponentTabId: () => loadedComponentTabId, - setLoadedComponentTabId: value => (loadedComponentTabId = value), - getLoadedStylesTabId: () => loadedStylesTabId, - setLoadedStylesTabId: value => (loadedStylesTabId = value), + isStyleWorkspaceTab, + clearTrackedWorkspaceTab, getWorkspaceTabByKind, makeUniqueTabPath, createWorkspaceTabId, @@ -915,12 +899,20 @@ const normalizeWorkspaceEditorsTrailingNewlineAfterPublish = getTabPublishPath: tab => getTabTargetPrFilePath(tab) || normalizeWorkspacePathValue(tab?.path) || '', normalizePublishPath: path => normalizeWorkspacePathValue(path), - getLoadedComponentTabId: () => loadedComponentTabId, - getLoadedStylesTabId: () => loadedStylesTabId, - getJsxSource: () => getJsxSource(), - getCssSource: () => getCssSource(), - setJsxSource, - setCssSource, + getActiveTabId: () => workspaceTabsState.getActiveTabId(), + getCurrentEditorSource: () => { + const activeTab = getActiveWorkspaceTab() + return isStyleWorkspaceTab(activeTab) ? getCssSource() : getJsxSource() + }, + setCurrentEditorSource: value => { + const activeTab = getActiveWorkspaceTab() + if (isStyleWorkspaceTab(activeTab)) { + setCssSource(value) + return + } + + setJsxSource(value) + }, setSuppressEditorChangeSideEffects: value => { suppressEditorChangeSideEffects = value }, @@ -1209,9 +1201,7 @@ chatDrawerController = githubWorkflows.chatDrawerController prDrawerController = githubWorkflows.prDrawerController workspacesDrawerController = githubWorkflows.workspacesDrawerController -const persistRenderMode = mode => persistRenderModeValue(mode, { renderModeStorageKey }) - -const getInitialRenderMode = () => getInitialRenderModeValue({ renderModeStorageKey }) +const getInitialRenderMode = () => 'dom' const updateRenderModeEditability = () => updateRenderModeEditabilityValue({ renderMode, getActiveWorkspaceTab }) @@ -1226,7 +1216,7 @@ const editorBootstrapOptions = createEditorBootstrapOptions({ styleMode, getSuppressEditorChangeSideEffects: () => suppressEditorChangeSideEffects, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, getDirtyStateForTabChange, workspaceTabsState, toWorkspaceSyncedContent, @@ -1287,7 +1277,7 @@ const runtimeCoreOptions = createRuntimeCoreOptions({ lintStylesButton, autoRenderToggle, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, getRenderRuntime: () => renderRuntime, getPreviewHost: () => previewHost, previewBackground, @@ -1299,7 +1289,6 @@ const runtimeCoreOptions = createRuntimeCoreOptions({ setPendingClearAction: value => (pendingClearAction = value), normalizeRenderMode, normalizeStyleMode, - persistRenderMode, resetDiagnosticsFlow: () => diagnosticsFlowController.resetDiagnosticsFlow(), maybeRender: () => diagnosticsFlowController.maybeRender(), flushWorkspaceSave, @@ -1436,15 +1425,10 @@ bindAppEventsAndStart({ updateRenderModeEditability, loadPreferredWorkspaceContext, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, setActiveWorkspaceTab, workspaceTabsState, - loadedStylesTabIdRef: { - get value() { - return loadedStylesTabId - }, - }, - getWorkspaceTabByKind, + getPrimaryStyleWorkspaceTab, syncDiagnosticsDrawerLayout, workspaceSaveController, workspaceStorage, diff --git a/src/modules/app-core/app-bindings-startup.js b/src/modules/app-core/app-bindings-startup.js index 534d6e8..c89242c 100644 --- a/src/modules/app-core/app-bindings-startup.js +++ b/src/modules/app-core/app-bindings-startup.js @@ -78,11 +78,9 @@ const bindAppEventsAndStart = ({ updateRenderModeEditability, loadPreferredWorkspaceContext, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, setActiveWorkspaceTab, - workspaceTabsState, - loadedStylesTabIdRef, - getWorkspaceTabByKind, + getPrimaryStyleWorkspaceTab, workspaceSaveController, workspaceStorage, syncDiagnosticsDrawerLayout, @@ -239,11 +237,11 @@ const bindAppEventsAndStart = ({ typecheckButton.addEventListener('click', () => { const { activeTab, tabsSnapshot } = syncAndCaptureDiagnosticsSnapshot() const source = - getTabKind(activeTab) === 'component' && typeof activeTab?.content === 'string' + !isStyleWorkspaceTab(activeTab) && typeof activeTab?.content === 'string' ? activeTab.content : getJsxSource() const sourcePath = - getTabKind(activeTab) === 'component' && typeof activeTab?.path === 'string' + !isStyleWorkspaceTab(activeTab) && typeof activeTab?.path === 'string' ? activeTab.path : getTypecheckSourcePath() @@ -259,7 +257,7 @@ const bindAppEventsAndStart = ({ lintComponentButton.addEventListener('click', () => { const { activeTab } = syncAndCaptureDiagnosticsSnapshot() const source = - getTabKind(activeTab) === 'component' && typeof activeTab?.content === 'string' + !isStyleWorkspaceTab(activeTab) && typeof activeTab?.content === 'string' ? activeTab.content : getJsxSource() @@ -273,7 +271,7 @@ const bindAppEventsAndStart = ({ lintStylesButton.addEventListener('click', () => { const { activeTab } = syncAndCaptureDiagnosticsSnapshot() const source = - getTabKind(activeTab) === 'styles' && typeof activeTab?.content === 'string' + isStyleWorkspaceTab(activeTab) && typeof activeTab?.content === 'string' ? activeTab.content : getCssSource() @@ -457,14 +455,14 @@ const bindAppEventsAndStart = ({ if (workspaceTabAddModule instanceof HTMLButtonElement) { workspaceTabAddModule.addEventListener('click', event => { event.stopPropagation() - addWorkspaceTab('component') + addWorkspaceTab({ type: 'script' }) }) } if (workspaceTabAddStyles instanceof HTMLButtonElement) { workspaceTabAddStyles.addEventListener('click', event => { event.stopPropagation() - addWorkspaceTab('styles') + addWorkspaceTab({ type: 'style' }) }) } @@ -498,11 +496,11 @@ const bindAppEventsAndStart = ({ syncDiagnosticsDrawerLayout() } - const stylesTab = - workspaceTabsState.getTab(loadedStylesTabIdRef.value) ?? - getWorkspaceTabByKind('styles') - if (stylesTab && typeof stylesTab.content === 'string') { - setCssSource(stylesTab.content) + if (!isStyleWorkspaceTab(activeTab)) { + const stylesTab = getPrimaryStyleWorkspaceTab() + if (stylesTab && typeof stylesTab.content === 'string') { + setCssSource(stylesTab.content) + } } setHasCompletedInitialWorkspaceBootstrap(true) diff --git a/src/modules/app-core/app-composition-options.js b/src/modules/app-core/app-composition-options.js index 2118dc5..5b163d9 100644 --- a/src/modules/app-core/app-composition-options.js +++ b/src/modules/app-core/app-composition-options.js @@ -32,7 +32,7 @@ const createRuntimeCoreOptions = ({ lintStylesButton, autoRenderToggle, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, getRenderRuntime, getPreviewHost, previewBackground, @@ -44,7 +44,6 @@ const createRuntimeCoreOptions = ({ setPendingClearAction, normalizeRenderMode, normalizeStyleMode, - persistRenderMode, resetDiagnosticsFlow, maybeRender, flushWorkspaceSave, @@ -88,7 +87,7 @@ const createRuntimeCoreOptions = ({ lintStylesButton, autoRenderToggle, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, getRenderRuntime, }, renderRuntimeOptions: { @@ -111,7 +110,6 @@ const createRuntimeCoreOptions = ({ setPendingClearAction, normalizeRenderMode, normalizeStyleMode, - persistRenderMode, resetDiagnosticsFlow, maybeRender, flushWorkspaceSave, @@ -121,7 +119,7 @@ const createRuntimeCoreOptions = ({ setSuppressEditorChangeSideEffects, getStyleEditorLanguage, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, workspaceTabsState, queueWorkspaceSave, }) @@ -136,7 +134,7 @@ const createEditorBootstrapOptions = ({ styleMode, getSuppressEditorChangeSideEffects, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, getDirtyStateForTabChange, workspaceTabsState, toWorkspaceSyncedContent, @@ -167,7 +165,7 @@ const createEditorBootstrapOptions = ({ getStyleModeValue: () => styleMode.value, getSuppressEditorChangeSideEffects, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, getDirtyStateForTabChange, workspaceTabsState, toWorkspaceSyncedContent, @@ -284,8 +282,6 @@ const createAppStartupBindingsOptions = ({ getActiveWorkspaceTab, setActiveWorkspaceTab, workspaceTabsState, - getLoadedStylesTabId, - getWorkspaceTabByKind, setHasCompletedInitialWorkspaceBootstrap, }) => ({ renderMode, @@ -382,12 +378,6 @@ const createAppStartupBindingsOptions = ({ getActiveWorkspaceTab, setActiveWorkspaceTab, workspaceTabsState, - loadedStylesTabIdRef: { - get value() { - return getLoadedStylesTabId() - }, - }, - getWorkspaceTabByKind, setHasCompletedInitialWorkspaceBootstrap, }) diff --git a/src/modules/app-core/diagnostics-flow-controller.js b/src/modules/app-core/diagnostics-flow-controller.js index 1b34445..99f0e99 100644 --- a/src/modules/app-core/diagnostics-flow-controller.js +++ b/src/modules/app-core/diagnostics-flow-controller.js @@ -30,7 +30,7 @@ const createDiagnosticsFlowController = ({ lintStylesButton, autoRenderToggle, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, getRenderRuntime, }) => { let activeComponentLintAbortController = null @@ -176,8 +176,8 @@ const createDiagnosticsFlowController = ({ if ( statusNode.textContent.startsWith('Rendered (Lint issues:') || - statusNode.textContent.startsWith('Linting component with Biome...') || - statusNode.textContent.startsWith('Linting styles with Biome...') + statusNode.textContent.startsWith('Linting ') || + statusNode.textContent.startsWith('Lint failed') ) { setStatus('Rendered', 'neutral') } @@ -398,7 +398,7 @@ const createDiagnosticsFlowController = ({ } const activeTab = getActiveWorkspaceTab() - if (activeTab && getTabKind(activeTab) === 'component') { + if (activeTab && !isStyleWorkspaceTab(activeTab)) { const shouldRender = getRenderRuntime()?.shouldAutoRenderForTabChange(activeTab.id) if (!shouldRender) { return diff --git a/src/modules/app-core/diagnostics-tab-state-helpers.js b/src/modules/app-core/diagnostics-tab-state-helpers.js index 29bfe04..7f6e37a 100644 --- a/src/modules/app-core/diagnostics-tab-state-helpers.js +++ b/src/modules/app-core/diagnostics-tab-state-helpers.js @@ -1,8 +1,8 @@ const createDiagnosticsTabStateHelpers = ({ getActiveWorkspaceTab, - getLoadedComponentWorkspaceTab, - getLoadedStylesWorkspaceTab, - getTabKind, + getEntryWorkspaceTab, + getPrimaryStyleWorkspaceTab, + isStyleWorkspaceTab, toNonEmptyWorkspaceText, diagnosticsComponentSection, diagnosticsStylesSection, @@ -20,7 +20,7 @@ const createDiagnosticsTabStateHelpers = ({ }) => { const syncDiagnosticsDrawerLayout = () => { const activeTab = getActiveWorkspaceTab() - const isStylesTab = getTabKind(activeTab) === 'styles' + const isStylesTab = isStyleWorkspaceTab(activeTab) if (diagnosticsComponentSection instanceof HTMLElement) { diagnosticsComponentSection.hidden = isStylesTab @@ -78,9 +78,7 @@ const createDiagnosticsTabStateHelpers = ({ const getComponentLintTarget = () => { const activeTab = getActiveWorkspaceTab() const tab = - activeTab && getTabKind(activeTab) === 'component' - ? activeTab - : getLoadedComponentWorkspaceTab() + activeTab && !isStyleWorkspaceTab(activeTab) ? activeTab : getEntryWorkspaceTab() if (!tab) { return null } @@ -95,9 +93,9 @@ const createDiagnosticsTabStateHelpers = ({ const getStylesLintTarget = () => { const activeTab = getActiveWorkspaceTab() const tab = - activeTab && getTabKind(activeTab) === 'styles' + activeTab && isStyleWorkspaceTab(activeTab) ? activeTab - : getLoadedStylesWorkspaceTab() + : getPrimaryStyleWorkspaceTab() if (!tab) { return null } diff --git a/src/modules/app-core/editor-bootstrap-controller.js b/src/modules/app-core/editor-bootstrap-controller.js index 6111f21..c122a37 100644 --- a/src/modules/app-core/editor-bootstrap-controller.js +++ b/src/modules/app-core/editor-bootstrap-controller.js @@ -8,7 +8,7 @@ const createEditorBootstrapController = ({ getStyleModeValue, getSuppressEditorChangeSideEffects, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, getDirtyStateForTabChange, workspaceTabsState, toWorkspaceSyncedContent, @@ -59,7 +59,7 @@ const createEditorBootstrapController = ({ return } const activeTab = getActiveWorkspaceTab() - if (activeTab && getTabKind(activeTab) === 'component') { + if (activeTab && !isStyleWorkspaceTab(activeTab)) { const nextContent = getJsxSource() const nextDirtyState = getDirtyStateForTabChange(activeTab, nextContent) workspaceTabsState.upsertTab( @@ -97,7 +97,7 @@ const createEditorBootstrapController = ({ return } const activeTab = getActiveWorkspaceTab() - if (activeTab && getTabKind(activeTab) === 'styles') { + if (activeTab && isStyleWorkspaceTab(activeTab)) { const nextContent = getCssSource() const nextDirtyState = getDirtyStateForTabChange(activeTab, nextContent) workspaceTabsState.upsertTab( diff --git a/src/modules/app-core/github-workflows.js b/src/modules/app-core/github-workflows.js index d4efcbd..b9ad301 100644 --- a/src/modules/app-core/github-workflows.js +++ b/src/modules/app-core/github-workflows.js @@ -357,10 +357,13 @@ const initializeGitHubWorkflows = ({ }, onSyncActivePrEditorContent: async args => { if (!shouldApplyActivePrEditorSync(args ?? {})) { + const tabTargets = Array.isArray(args?.syncTargets?.tabTargets) + ? args.syncTargets.tabTargets + : [] return { synced: false, - componentSynced: false, - stylesSynced: false, + syncedTabCount: 0, + totalTabCount: tabTargets.length, } } diff --git a/src/modules/app-core/publish-trailing-newline-normalizer.js b/src/modules/app-core/publish-trailing-newline-normalizer.js index 7629a42..93c803f 100644 --- a/src/modules/app-core/publish-trailing-newline-normalizer.js +++ b/src/modules/app-core/publish-trailing-newline-normalizer.js @@ -10,12 +10,9 @@ const createPublishTrailingNewlineNormalizer = ({ workspaceTabsState, getTabPublishPath, normalizePublishPath, - getLoadedComponentTabId, - getLoadedStylesTabId, - getJsxSource, - getCssSource, - setJsxSource, - setCssSource, + getActiveTabId, + getCurrentEditorSource, + setCurrentEditorSource, setSuppressEditorChangeSideEffects, queueWorkspaceSave, }) => { @@ -37,7 +34,7 @@ const createPublishTrailingNewlineNormalizer = ({ } const tabs = workspaceTabsState.getTabs() - const activeTabId = workspaceTabsState.getActiveTabId() + const activeTabIdBeforeUpdate = workspaceTabsState.getActiveTabId() const now = Date.now() let didUpdateTabs = false const updatedContentByTabId = new Map() @@ -67,32 +64,31 @@ const createPublishTrailingNewlineNormalizer = ({ }) if (didUpdateTabs) { - workspaceTabsState.replaceTabs({ tabs: nextTabs, activeTabId }) + workspaceTabsState.replaceTabs({ + tabs: nextTabs, + activeTabId: activeTabIdBeforeUpdate, + }) } - const loadedComponentTabId = - typeof getLoadedComponentTabId === 'function' ? getLoadedComponentTabId() : '' - const nextJsxSource = loadedComponentTabId - ? updatedContentByTabId.get(loadedComponentTabId) + const resolvedActiveTabId = + typeof getActiveTabId === 'function' + ? getActiveTabId() + : workspaceTabsState.getActiveTabId() || activeTabIdBeforeUpdate + const nextActiveEditorSource = resolvedActiveTabId + ? updatedContentByTabId.get(resolvedActiveTabId) : null - if (typeof nextJsxSource === 'string' && nextJsxSource !== getJsxSource()) { - setSuppressEditorChangeSideEffects(true) - try { - setJsxSource(nextJsxSource) - } finally { - setSuppressEditorChangeSideEffects(false) - } - } + const currentEditorSource = + typeof getCurrentEditorSource === 'function' ? getCurrentEditorSource() : null - const loadedStylesTabId = - typeof getLoadedStylesTabId === 'function' ? getLoadedStylesTabId() : '' - const nextCssSource = loadedStylesTabId - ? updatedContentByTabId.get(loadedStylesTabId) - : null - if (typeof nextCssSource === 'string' && nextCssSource !== getCssSource()) { + if ( + typeof nextActiveEditorSource === 'string' && + typeof currentEditorSource === 'string' && + nextActiveEditorSource !== currentEditorSource && + typeof setCurrentEditorSource === 'function' + ) { setSuppressEditorChangeSideEffects(true) try { - setCssSource(nextCssSource) + setCurrentEditorSource(nextActiveEditorSource) } finally { setSuppressEditorChangeSideEffects(false) } diff --git a/src/modules/app-core/runtime-core-setup.js b/src/modules/app-core/runtime-core-setup.js index 84a0db1..6f8d1e7 100644 --- a/src/modules/app-core/runtime-core-setup.js +++ b/src/modules/app-core/runtime-core-setup.js @@ -10,7 +10,6 @@ const createRuntimeCoreSetup = ({ setPendingClearAction, normalizeRenderMode, normalizeStyleMode, - persistRenderMode, resetDiagnosticsFlow, maybeRender, flushWorkspaceSave, @@ -20,7 +19,7 @@ const createRuntimeCoreSetup = ({ setSuppressEditorChangeSideEffects, getStyleEditorLanguage, getActiveWorkspaceTab, - getTabKind, + isStyleWorkspaceTab, workspaceTabsState, queueWorkspaceSave, }) => { @@ -99,7 +98,7 @@ const createRuntimeCoreSetup = ({ renderMode.value = nextMode } - persistRenderMode(nextMode) + queueWorkspaceSave() resetDiagnosticsFlow() maybeRender() @@ -128,7 +127,7 @@ const createRuntimeCoreSetup = ({ } const activeTab = getActiveWorkspaceTab() - if (activeTab && getTabKind(activeTab) === 'styles') { + if (activeTab && isStyleWorkspaceTab(activeTab)) { const nextLanguage = nextMode === 'less' ? 'less' diff --git a/src/modules/app-core/runtime-editor-utils.js b/src/modules/app-core/runtime-editor-utils.js index 60c4f0e..07ef0f6 100644 --- a/src/modules/app-core/runtime-editor-utils.js +++ b/src/modules/app-core/runtime-editor-utils.js @@ -6,27 +6,6 @@ const getStyleEditorLanguage = mode => { const normalizeRenderMode = mode => (mode === 'react' ? 'react' : 'dom') -const persistRenderMode = (mode, { renderModeStorageKey }) => { - const normalizedMode = normalizeRenderMode(mode) - - try { - localStorage.setItem(renderModeStorageKey, normalizedMode) - } catch { - /* Ignore storage write errors in restricted browsing modes. */ - } -} - -const getInitialRenderMode = ({ renderModeStorageKey }) => { - try { - const value = localStorage.getItem(renderModeStorageKey) - return normalizeRenderMode(value) - } catch { - /* Ignore storage read errors in restricted browsing modes. */ - } - - return 'dom' -} - const updateRenderModeEditability = ({ renderMode, getActiveWorkspaceTab }) => { if (!(renderMode instanceof HTMLSelectElement)) { return @@ -79,11 +58,9 @@ const setCssSourceValue = ({ } export { - getInitialRenderMode, getStyleEditorLanguage, normalizeRenderMode, normalizeStyleMode, - persistRenderMode, setCssSourceValue, setJsxSourceValue, updateRenderModeEditability, diff --git a/src/modules/app-core/workspace-context-controller.js b/src/modules/app-core/workspace-context-controller.js index 3529438..f08c056 100644 --- a/src/modules/app-core/workspace-context-controller.js +++ b/src/modules/app-core/workspace-context-controller.js @@ -19,7 +19,6 @@ const createWorkspaceContextController = ({ normalizeRenderMode, getRenderModeValue, setRenderModeValue, - persistRenderMode, onWorkspaceRecordApplied, getActiveWorkspaceTab, loadWorkspaceTabIntoEditor, @@ -129,7 +128,6 @@ const createWorkspaceContextController = ({ if (getRenderModeValue() !== nextRenderMode) { setRenderModeValue(nextRenderMode) } - persistRenderMode(nextRenderMode) const activeTab = getActiveWorkspaceTab() if (activeTab) { diff --git a/src/modules/app-core/workspace-controllers-setup.js b/src/modules/app-core/workspace-controllers-setup.js index 53e0ce1..e40eb07 100644 --- a/src/modules/app-core/workspace-controllers-setup.js +++ b/src/modules/app-core/workspace-controllers-setup.js @@ -30,7 +30,6 @@ const createWorkspaceControllersSetup = ({ normalizeRenderMode, getRenderModeValue, setRenderModeValue, - persistRenderMode, onWorkspaceRecordApplied, getActiveWorkspaceTab, onActiveWorkspaceTabChange, @@ -66,11 +65,8 @@ const createWorkspaceControllersSetup = ({ syncHeaderLabels, setWorkspaceTabAddMenuOpen, confirmAction, - getTabKind, - getLoadedComponentTabId, - setLoadedComponentTabId, - getLoadedStylesTabId, - setLoadedStylesTabId, + isStyleWorkspaceTab, + clearTrackedWorkspaceTab, getWorkspaceTabByKind, makeUniqueTabPath, createWorkspaceTabId, @@ -151,12 +147,9 @@ const createWorkspaceControllersSetup = ({ maybeRender: () => maybeRender(), setWorkspaceTabAddMenuOpen, confirmAction, - getTabKind, + isStyleWorkspaceTab, persistActiveTabEditorContent, - getLoadedComponentTabId, - setLoadedComponentTabId, - getLoadedStylesTabId, - setLoadedStylesTabId, + clearTrackedWorkspaceTab, getActiveWorkspaceTab, loadWorkspaceTabIntoEditor, getWorkspaceTabByKind, @@ -226,7 +219,6 @@ const createWorkspaceControllersSetup = ({ normalizeRenderMode, getRenderModeValue, setRenderModeValue, - persistRenderMode, onWorkspaceRecordApplied, getActiveWorkspaceTab, loadWorkspaceTabIntoEditor, @@ -249,7 +241,8 @@ const createWorkspaceControllersSetup = ({ const applyWorkspaceRecord = async (workspace, { silent = false } = {}) => workspaceContextController.applyWorkspaceRecord(workspace, { silent }) - const addWorkspaceTab = kind => workspaceTabMutationsController.addWorkspaceTab(kind) + const addWorkspaceTab = request => + workspaceTabMutationsController.addWorkspaceTab(request) const loadPreferredWorkspaceContext = async () => workspaceContextController.loadPreferredWorkspaceContext() diff --git a/src/modules/app-core/workspace-editor-helpers.js b/src/modules/app-core/workspace-editor-helpers.js index 22b8935..a1b1bca 100644 --- a/src/modules/app-core/workspace-editor-helpers.js +++ b/src/modules/app-core/workspace-editor-helpers.js @@ -2,7 +2,7 @@ import { isTabEditedForDisplay } from './workspace-tab-edited-display.js' const createWorkspaceEditorHelpers = ({ workspaceTabsState, - getTabKind, + isStyleWorkspaceTab, editorKinds, editorPanelsByKind, editorHeaderLabelByKind, @@ -10,10 +10,8 @@ const createWorkspaceEditorHelpers = ({ getShouldShowEditedDesign, defaultTabNameByKind, toNonEmptyWorkspaceText, - getLoadedStylesTabId, - getLoadedComponentTabId, - setLoadedStylesTabId, - setLoadedComponentTabId, + getEntryWorkspaceTab, + getPrimaryStyleWorkspaceTab, getCssSource, getJsxSource, getDirtyStateForTabChange, @@ -26,28 +24,56 @@ const createWorkspaceEditorHelpers = ({ setSuppressEditorChangeSideEffects, editorPool, }) => { + const trackedTabIdByKind = { + component: toNonEmptyWorkspaceText(getEntryWorkspaceTab()?.id), + styles: toNonEmptyWorkspaceText(getPrimaryStyleWorkspaceTab()?.id), + } + + const resolveTabForKind = kind => { + if (kind !== 'styles' && kind !== 'component') { + return null + } + + const isStyleKind = kind === 'styles' + + const activeTab = workspaceTabsState.getTab(workspaceTabsState.getActiveTabId()) + if (activeTab && isStyleWorkspaceTab(activeTab) === isStyleKind) { + return activeTab + } + + const trackedTabId = toNonEmptyWorkspaceText(trackedTabIdByKind[kind]) + const trackedTab = trackedTabId ? workspaceTabsState.getTab(trackedTabId) : null + if (trackedTab && isStyleWorkspaceTab(trackedTab) === isStyleKind) { + return trackedTab + } + + return kind === 'styles' ? getPrimaryStyleWorkspaceTab() : getEntryWorkspaceTab() + } + const getWorkspaceTabByKind = kind => { - const tabs = workspaceTabsState.getTabs() - const normalizedKind = kind === 'styles' ? 'styles' : 'component' - return ( - tabs.find( - tab => - getTabKind(tab) === normalizedKind && - tab.id === workspaceTabsState.getActiveTabId(), - ) ?? - tabs.find(tab => getTabKind(tab) === normalizedKind) ?? - null - ) + return resolveTabForKind(kind) + } + + const clearTrackedWorkspaceTab = tabId => { + const normalizedTabId = toNonEmptyWorkspaceText(tabId) + if (!normalizedTabId) { + return + } + + if (trackedTabIdByKind.component === normalizedTabId) { + trackedTabIdByKind.component = toNonEmptyWorkspaceText(getEntryWorkspaceTab()?.id) + } + + if (trackedTabIdByKind.styles === normalizedTabId) { + trackedTabIdByKind.styles = toNonEmptyWorkspaceText( + getPrimaryStyleWorkspaceTab()?.id, + ) + } } const syncHeaderLabels = () => { for (const editorKind of editorKinds) { - const tab = - editorKind === 'styles' - ? (workspaceTabsState.getTab(getLoadedStylesTabId()) ?? - getWorkspaceTabByKind('styles')) - : (workspaceTabsState.getTab(getLoadedComponentTabId()) ?? - getWorkspaceTabByKind('component')) + const tab = resolveTabForKind(editorKind) const headerLabel = editorHeaderLabelByKind[editorKind] const dirtyStatusLabel = editorHeaderDirtyStatusByKind[editorKind] @@ -79,26 +105,19 @@ const createWorkspaceEditorHelpers = ({ return } - const activeTabKind = getTabKind(activeTab) - const loadedTabId = - activeTabKind === 'styles' ? getLoadedStylesTabId() : getLoadedComponentTabId() - const loadedTab = loadedTabId ? workspaceTabsState.getTab(loadedTabId) : null - const targetTab = - loadedTab && getTabKind(loadedTab) === activeTabKind ? loadedTab : activeTab - - const nextContent = activeTabKind === 'styles' ? getCssSource() : getJsxSource() + const nextContent = isStyleWorkspaceTab(activeTab) ? getCssSource() : getJsxSource() - if (nextContent === targetTab.content) { + if (nextContent === activeTab.content) { return } workspaceTabsState.upsertTab( { - ...targetTab, + ...activeTab, content: nextContent, - isDirty: getDirtyStateForTabChange(targetTab, nextContent), + isDirty: getDirtyStateForTabChange(activeTab, nextContent), lastModified: Date.now(), - isActive: targetTab.id === activeTab.id, + isActive: true, }, { emitReason: 'tabContentSync' }, ) @@ -149,8 +168,8 @@ const createWorkspaceEditorHelpers = ({ } } - if (getTabKind(tab) === 'styles') { - setLoadedStylesTabId(tab.id) + if (isStyleWorkspaceTab(tab)) { + trackedTabIdByKind.styles = tab.id setSuppressEditorChangeSideEffects(true) try { setCssSource(nextContent) @@ -161,14 +180,12 @@ const createWorkspaceEditorHelpers = ({ setVisibleEditorPanelForKind('styles') editorPool.activate('styles') } else { - setLoadedComponentTabId(tab.id) + trackedTabIdByKind.component = tab.id setSuppressEditorChangeSideEffects(true) try { setJsxSource(nextContent) - const stylesTab = - workspaceTabsState.getTab(getLoadedStylesTabId()) ?? - getWorkspaceTabByKind('styles') + const stylesTab = resolveTabForKind('styles') if (stylesTab) { applyStyleLanguage(stylesTab.language) } @@ -184,6 +201,7 @@ const createWorkspaceEditorHelpers = ({ } return { + clearTrackedWorkspaceTab, getWorkspaceTabByKind, syncHeaderLabels, persistActiveTabEditorContent, diff --git a/src/modules/app-core/workspace-pr-session-handoff-controller.js b/src/modules/app-core/workspace-pr-session-handoff-controller.js index 4e55b58..a977933 100644 --- a/src/modules/app-core/workspace-pr-session-handoff-controller.js +++ b/src/modules/app-core/workspace-pr-session-handoff-controller.js @@ -52,7 +52,7 @@ export const createWorkspacePrSessionHandoffController = ({ const now = Date.now() return { - id: 'component', + id: 'entry', name: defaultComponentTabName, path: defaultComponentTabPath, language: 'javascript-jsx', @@ -103,7 +103,7 @@ export const createWorkspacePrSessionHandoffController = ({ workspaceTabsState.replaceTabs({ tabs: [createFreshLocalEntryTab()], - activeTabId: 'component', + activeTabId: 'entry', }) const activeTab = getActiveWorkspaceTab() diff --git a/src/modules/app-core/workspace-sync-controller.js b/src/modules/app-core/workspace-sync-controller.js index aa1cce0..a4c9175 100644 --- a/src/modules/app-core/workspace-sync-controller.js +++ b/src/modules/app-core/workspace-sync-controller.js @@ -1,6 +1,6 @@ const createWorkspaceSyncController = ({ workspaceTabsState, - getTabKind, + isStyleWorkspaceTab, getTabTargetPrFilePath, normalizeWorkspacePathValue, toWorkspaceSyncedContent, @@ -35,7 +35,7 @@ const createWorkspaceSyncController = ({ const currentContent = tab.id === activeTabId - ? getTabKind(tab) === 'styles' + ? isStyleWorkspaceTab(tab) ? getCssSource() : getJsxSource() : typeof tab.content === 'string' @@ -192,7 +192,7 @@ const createWorkspaceSyncController = ({ dedupedByPath.set(path, { path, - kind: getTabKind(tab), + kind: isStyleWorkspaceTab(tab) ? 'styles' : 'component', tabId: toNonEmptyWorkspaceText(tab?.id), }) } diff --git a/src/modules/app-core/workspace-tab-mutations-controller.js b/src/modules/app-core/workspace-tab-mutations-controller.js index 227418d..b5c5694 100644 --- a/src/modules/app-core/workspace-tab-mutations-controller.js +++ b/src/modules/app-core/workspace-tab-mutations-controller.js @@ -16,12 +16,9 @@ const createWorkspaceTabMutationsController = ({ maybeRender, setWorkspaceTabAddMenuOpen, confirmAction, - getTabKind, + isStyleWorkspaceTab, persistActiveTabEditorContent, - getLoadedComponentTabId, - setLoadedComponentTabId, - getLoadedStylesTabId, - setLoadedStylesTabId, + clearTrackedWorkspaceTab, getActiveWorkspaceTab, loadWorkspaceTabIntoEditor, getWorkspaceTabByKind, @@ -30,6 +27,38 @@ const createWorkspaceTabMutationsController = ({ createWorkspaceTabId, getShouldShowEditedDesign, }) => { + const moduleTabTemplates = { + script: { + basePath: 'src/components/module.tsx', + language: 'javascript-jsx', + idPrefix: 'module', + defaultName: 'script-tab', + statusMessage: 'Added JavaScript tab.', + }, + style: { + basePath: 'src/styles/module.css', + language: 'css', + idPrefix: 'style', + defaultName: 'style-tab', + statusMessage: 'Added style tab.', + }, + } + + const resolveModuleTabTemplate = request => { + if (request && typeof request === 'object') { + const type = toNonEmptyWorkspaceText(request.type).toLowerCase() + if (type === 'style') { + return moduleTabTemplates.style + } + + if (type === 'script') { + return moduleTabTemplates.script + } + } + + return null + } + const beginWorkspaceTabRename = tabId => { setWorkspaceTabAddMenuOpen(false) setWorkspaceTabRenameState({ @@ -142,25 +171,15 @@ const createWorkspaceTabMutationsController = ({ copy: 'This removes the tab and its local source content from this workspace context.', confirmButtonText: 'Remove tab', onConfirm: () => { - const removedKind = getTabKind(tab) + const removedKind = isStyleWorkspaceTab(tab) ? 'styles' : 'component' persistActiveTabEditorContent() const removed = workspaceTabsState.removeTab(tab.id) if (!removed) { return } - if (getLoadedComponentTabId() === tab.id) { - setLoadedComponentTabId( - workspaceTabsState.getTabs().find(entry => getTabKind(entry) === 'component') - ?.id || 'component', - ) - } - - if (getLoadedStylesTabId() === tab.id) { - setLoadedStylesTabId( - workspaceTabsState.getTabs().find(entry => getTabKind(entry) === 'styles') - ?.id || 'styles', - ) + if (typeof clearTrackedWorkspaceTab === 'function') { + clearTrackedWorkspaceTab(tab.id) } const activeTab = getActiveWorkspaceTab() @@ -168,9 +187,7 @@ const createWorkspaceTabMutationsController = ({ loadWorkspaceTabIntoEditor(activeTab) } else { const fallbackTab = - getWorkspaceTabByKind(removedKind === 'styles' ? 'component' : 'styles') || - workspaceTabsState.getTabs()[0] || - null + getWorkspaceTabByKind(removedKind) || workspaceTabsState.getTabs()[0] || null if (fallbackTab) { setActiveWorkspaceTab(fallbackTab.id) } @@ -189,20 +206,16 @@ const createWorkspaceTabMutationsController = ({ }) } - const addWorkspaceTab = kind => { - const normalizedKind = - kind === 'styles' ? 'styles' : kind === 'component' ? 'component' : '' - if (!normalizedKind) { - setStatus('Choose a tab type before adding a tab.', 'neutral') + const addWorkspaceTab = request => { + const template = resolveModuleTabTemplate(request) + if (!template) { + setStatus('Choose a tab template before adding a tab.', 'neutral') return } - const basePath = - normalizedKind === 'styles' ? 'src/styles/module.css' : 'src/components/module.tsx' - const language = normalizedKind === 'styles' ? 'css' : 'javascript-jsx' - const path = makeUniqueTabPath({ basePath }) - const tabId = createWorkspaceTabId(normalizedKind === 'styles' ? 'style' : 'module') - const name = getPathFileName(path) || `${normalizedKind}-tab` + const path = makeUniqueTabPath({ basePath: template.basePath }) + const tabId = createWorkspaceTabId(template.idPrefix) + const name = getPathFileName(path) || template.defaultName const shouldMarkNewTabEdited = typeof getShouldShowEditedDesign === 'function' ? Boolean(getShouldShowEditedDesign()) @@ -214,7 +227,7 @@ const createWorkspaceTabMutationsController = ({ id: tabId, name, path, - language, + language: template.language, role: 'module', isActive: false, content: '', @@ -224,12 +237,7 @@ const createWorkspaceTabMutationsController = ({ setWorkspaceTabAddMenuOpen(false) setActiveWorkspaceTab(tabId) - - if (normalizedKind === 'styles') { - setStatus('Added style tab.', 'neutral') - } else { - setStatus('Added JavaScript tab.', 'neutral') - } + setStatus(template.statusMessage, 'neutral') } return { diff --git a/src/modules/app-core/workspace-tab-selectors.js b/src/modules/app-core/workspace-tab-selectors.js new file mode 100644 index 0000000..22562e4 --- /dev/null +++ b/src/modules/app-core/workspace-tab-selectors.js @@ -0,0 +1,28 @@ +const createWorkspaceTabSelectors = ({ + workspaceTabsState, + getTabKind, + toNonEmptyWorkspaceText, +}) => { + const getActiveWorkspaceTab = () => + workspaceTabsState.getTab(workspaceTabsState.getActiveTabId()) + + const getEntryWorkspaceTab = () => + workspaceTabsState.getTabs().find(tab => tab?.role === 'entry') ?? null + + const getPrimaryStyleWorkspaceTab = () => + workspaceTabsState.getTabs().find(tab => getTabKind(tab) === 'styles') ?? null + + const getInitialLoadedTabIds = () => ({ + componentTabId: toNonEmptyWorkspaceText(getEntryWorkspaceTab()?.id), + stylesTabId: toNonEmptyWorkspaceText(getPrimaryStyleWorkspaceTab()?.id), + }) + + return { + getActiveWorkspaceTab, + getEntryWorkspaceTab, + getPrimaryStyleWorkspaceTab, + getInitialLoadedTabIds, + } +} + +export { createWorkspaceTabSelectors } diff --git a/src/modules/github/pr/editor-sync.js b/src/modules/github/pr/editor-sync.js index b376036..b4400c5 100644 --- a/src/modules/github/pr/editor-sync.js +++ b/src/modules/github/pr/editor-sync.js @@ -40,8 +40,8 @@ export const createGitHubPrEditorSyncController = ({ shouldApplySyncResult }) => if (!token || !owner || !repo || !branch || normalizedTabTargets.length === 0) { return { synced: false, - componentSynced: false, - stylesSynced: false, + syncedTabCount: 0, + totalTabCount: normalizedTabTargets.length, } } @@ -56,8 +56,8 @@ export const createGitHubPrEditorSyncController = ({ shouldApplySyncResult }) => ) { return { synced: false, - componentSynced: false, - stylesSynced: false, + syncedTabCount: 0, + totalTabCount: normalizedTabTargets.length, } } @@ -84,8 +84,8 @@ export const createGitHubPrEditorSyncController = ({ shouldApplySyncResult }) => if (signal?.aborted) { return { synced: false, - componentSynced: false, - stylesSynced: false, + syncedTabCount: 0, + totalTabCount: normalizedTabTargets.length, } } @@ -100,8 +100,8 @@ export const createGitHubPrEditorSyncController = ({ shouldApplySyncResult }) => ) { return { synced: false, - componentSynced: false, - stylesSynced: false, + syncedTabCount: 0, + totalTabCount: normalizedTabTargets.length, } } @@ -113,23 +113,11 @@ export const createGitHubPrEditorSyncController = ({ shouldApplySyncResult }) => content: target.content, })) - const componentTargets = requestedTargets.filter( - target => target.kind === 'component', - ) - const stylesTargets = requestedTargets.filter(target => target.kind === 'styles') - const componentSynced = - componentTargets.length > 0 && - componentTargets.every(target => typeof target.content === 'string') - const stylesSynced = - stylesTargets.length > 0 && - stylesTargets.every(target => typeof target.content === 'string') const allTargetsSynced = syncedTabTargets.length === normalizedTabTargets.length if (!allTargetsSynced) { return { synced: false, - componentSynced, - stylesSynced, syncedTabCount: syncedTabTargets.length, totalTabCount: normalizedTabTargets.length, syncTargets: { @@ -140,8 +128,6 @@ export const createGitHubPrEditorSyncController = ({ shouldApplySyncResult }) => return { synced: true, - componentSynced, - stylesSynced, syncedTabCount: syncedTabTargets.length, totalTabCount: normalizedTabTargets.length, syncTargets: { diff --git a/src/modules/preview-runtime/iframe-preview-executor.js b/src/modules/preview-runtime/iframe-preview-executor.js index 981dbae..abd3d38 100644 --- a/src/modules/preview-runtime/iframe-preview-executor.js +++ b/src/modules/preview-runtime/iframe-preview-executor.js @@ -168,11 +168,32 @@ const createIframeShellDocument = ({ channelId, parentOrigin, importMap }) => { } const __knightedEmitRuntimeError = details => { - const isMissingReference = - typeof details.message === 'string' && - details.message.toLowerCase().includes(' is not defined') + const missingReferenceName = (() => { + if (typeof details.message !== 'string') { + return '' + } + + const normalizedMessage = details.message.trim() + const missingReferenceMatch = normalizedMessage.match( + /^([A-Za-z_$][\\w$]*) is not defined\\b/i, + ) + if (missingReferenceMatch?.[1]) { + return missingReferenceMatch[1] + } + + const missingVariableMatch = normalizedMessage.match( + /^can't find variable:\\s*([A-Za-z_$][\\w$]*)\\b/i, + ) + return missingVariableMatch?.[1] ?? '' + })() + + const isLikelyTransientReference = + missingReferenceName.length > 0 && + missingReferenceName.length <= 3 && + missingReferenceName === missingReferenceName.toLowerCase() const isTransientOrigin = details.origin === 'window-error' || details.origin === 'promise' - if (isMissingReference && isTransientOrigin) { + + if (isLikelyTransientReference && isTransientOrigin) { return } diff --git a/src/modules/preview-runtime/virtual-workspace-modules.js b/src/modules/preview-runtime/virtual-workspace-modules.js index e9f7816..e80ca9e 100644 --- a/src/modules/preview-runtime/virtual-workspace-modules.js +++ b/src/modules/preview-runtime/virtual-workspace-modules.js @@ -1,6 +1,5 @@ import { isRelativeSpecifier, - stripImportDeclarationsBy, toModuleSpecifierKey, toTabModuleKey, } from './workspace-hydration.js' @@ -412,6 +411,16 @@ const rewriteImportSpecifiers = ({ source, imports, resolveSpecifier }) => { const toModuleDataUrl = code => `data:text/javascript;charset=utf-8,${encodeURIComponent(code)}` +const toStyleModuleDataUrl = ({ moduleKey, styleModuleExports }) => { + const safeModuleExports = + styleModuleExports && typeof styleModuleExports === 'object' ? styleModuleExports : {} + + const sourceUrl = `//# sourceURL=knighted-workspace/${moduleKey || 'styles'}.style.mjs` + return toModuleDataUrl( + `const __knightedStyles = ${JSON.stringify(safeModuleExports)}\nexport default __knightedStyles\n${sourceUrl}`, + ) +} + const runtimeSpecifierRewrites = runtimeSpecifiers => ({ react: runtimeSpecifiers.react, 'react-dom/client': runtimeSpecifiers.reactDomClient, @@ -552,6 +561,7 @@ export const planWorkspaceVirtualModules = ({ workspaceGraphCache, mode, runtimeSpecifiers, + styleModuleExportsByTabId = {}, }) => { if (!entryTab || typeof entryTab.content !== 'string') { return null @@ -681,6 +691,7 @@ export const planWorkspaceVirtualModules = ({ } const moduleDataByTabId = new Map() + const styleModuleUrlByTabId = new Map() for (const tabId of moduleDependencyOrder) { const tab = byId.get(tabId) @@ -704,6 +715,36 @@ export const planWorkspaceVirtualModules = ({ }) } + for (const tabId of styleDependencyOrder) { + const tab = byId.get(tabId) + if (!tab) { + continue + } + + const moduleKey = toTabModuleKey(tab) || tab.id + const styleModuleExports = + styleModuleExportsByTabId && typeof styleModuleExportsByTabId === 'object' + ? styleModuleExportsByTabId[tabId] + : {} + const moduleCacheKey = [ + 'style-module', + moduleKey, + JSON.stringify(styleModuleExports ?? {}), + ].join('\u0000') + const cachedModuleUrl = getCachedValue(moduleDataUrlCache, moduleCacheKey) + const moduleUrl = + typeof cachedModuleUrl === 'string' + ? cachedModuleUrl + : toStyleModuleDataUrl({ moduleKey, styleModuleExports }) + + if (!cachedModuleUrl) { + moduleDataUrlCache.set(moduleCacheKey, moduleUrl) + trimCache(moduleDataUrlCache, maxModuleDataUrlCacheEntries) + } + + styleModuleUrlByTabId.set(tabId, moduleUrl) + } + const runtimeRewrites = runtimeSpecifierRewrites(runtimeSpecifiers) const moduleUrlByTabId = new Map() @@ -715,35 +756,14 @@ export const planWorkspaceVirtualModules = ({ continue } - const sourceWithoutStyleImports = stripImportDeclarationsBy( - moduleData.source, - moduleData.imports, - entry => { - if ( - !isRelativeSpecifier(entry?.source) && - !isStyleImportSpecifier(entry?.source) - ) { - return false - } - - const target = resolveWorkspaceImport({ - importerModuleKey: moduleData.moduleKey, - source: entry.source, - byModuleKey, - }) - - return isStyleTab(target) - }, - ) - const importsForRewrite = parseImports({ - source: sourceWithoutStyleImports, + source: moduleData.source, transformJsxSource, formatTransformDiagnosticsError, }) const rewrittenCode = rewriteImportSpecifiers({ - source: sourceWithoutStyleImports, + source: moduleData.source, imports: importsForRewrite, resolveSpecifier: sourceSpecifier => { if ( @@ -761,7 +781,7 @@ export const planWorkspaceVirtualModules = ({ } if (isStyleTab(target)) { - return null + return styleModuleUrlByTabId.get(target.id) ?? null } return moduleUrlByTabId.get(target.id) ?? null diff --git a/src/modules/preview/render-runtime.js b/src/modules/preview/render-runtime.js index 77d406f..93f5a43 100644 --- a/src/modules/preview/render-runtime.js +++ b/src/modules/preview/render-runtime.js @@ -406,7 +406,7 @@ export const createRenderRuntimeController = ({ if (!entryTab) { clearStyleDiagnostics() - return { css: '', moduleExports: null } + return { css: '', styleModuleExportsByTabId: {} } } const runtimeSpecifiers = getWorkspaceRuntimeSpecifiers() @@ -422,7 +422,7 @@ export const createRenderRuntimeController = ({ if (!virtualModulePlan) { clearStyleDiagnostics() - return { css: '', moduleExports: null } + return { css: '', styleModuleExportsByTabId: {} } } const workspaceTabById = new Map( @@ -476,7 +476,7 @@ export const createRenderRuntimeController = ({ if (styleInputs.length === 0) { clearStyleDiagnostics() - const output = { css: '', moduleExports: null } + const output = { css: '', styleModuleExportsByTabId: {} } compiledStylesCache = { key: cacheKey, value: output, @@ -499,10 +499,13 @@ export const createRenderRuntimeController = ({ needsLightningCss ? ensureLightningCssWasm() : Promise.resolve(null), ]) - const compiledCssParts = await Promise.all( + const compiledStyleParts = await Promise.all( styleInputs.map(async input => { if (input.dialect === 'css') { - return input.source + return { + css: input.source, + moduleExports: null, + } } const options = { @@ -540,15 +543,53 @@ export const createRenderRuntimeController = ({ } const moduleExports = result.exports ?? null - return input.dialect === 'module' - ? appendCssModuleLocalAliases(result.css, moduleExports) - : result.css + return { + css: + input.dialect === 'module' + ? appendCssModuleLocalAliases(result.css, moduleExports) + : result.css, + moduleExports, + } }), ) + const styleModuleExportsByTabId = {} + const compiledCssParts = [] + + for (let index = 0; index < styleInputs.length; index += 1) { + const input = styleInputs[index] + const part = compiledStyleParts[index] + + if (part && typeof part.css === 'string') { + compiledCssParts.push(part.css) + } + + if (input?.dialect !== 'module' || !part?.moduleExports) { + continue + } + + const normalizedModuleExports = {} + for (const [localClassName, exportedValue] of Object.entries( + part.moduleExports, + )) { + if (typeof localClassName !== 'string' || localClassName.length === 0) { + continue + } + + const normalizedValue = normalizeCssModuleExport(exportedValue) + if (!normalizedValue) { + continue + } + + normalizedModuleExports[localClassName] = normalizedValue + } + + styleModuleExportsByTabId[input.id] = normalizedModuleExports + } + const output = { css: compiledCssParts.join('\n\n'), - moduleExports: null, + styleModuleExportsByTabId, } if (styleWarningLines.length > 0) { setStyleDiagnosticsDetails({ @@ -656,7 +697,11 @@ export const createRenderRuntimeController = ({ reactDomClient: getRuntimeSpecifier('reactDomClient'), }) - const renderWorkspaceInIframe = async ({ mode, cssText }) => { + const renderWorkspaceInIframe = async ({ + mode, + cssText, + styleModuleExportsByTabId = {}, + }) => { const workspaceTabs = getWorkspaceTabsForPreview() const entryTab = resolveWorkspaceEntryTab(workspaceTabs) @@ -686,6 +731,7 @@ export const createRenderRuntimeController = ({ workspaceGraphCache, mode, runtimeSpecifiers, + styleModuleExportsByTabId, }) if (!virtualModulePlan) { @@ -755,6 +801,7 @@ export const createRenderRuntimeController = ({ await renderWorkspaceInIframe({ mode: 'dom', cssText: compiledStyles.css, + styleModuleExportsByTabId: compiledStyles.styleModuleExportsByTabId, }) } @@ -772,6 +819,7 @@ export const createRenderRuntimeController = ({ await renderWorkspaceInIframe({ mode: 'react', cssText: compiledStyles.css, + styleModuleExportsByTabId: compiledStyles.styleModuleExportsByTabId, }) } diff --git a/src/modules/workspace/workspace-tab-helpers.js b/src/modules/workspace/workspace-tab-helpers.js index d206bf1..d0b2829 100644 --- a/src/modules/workspace/workspace-tab-helpers.js +++ b/src/modules/workspace/workspace-tab-helpers.js @@ -233,8 +233,14 @@ const resolveWorkspaceActiveTabId = ({ tabs, requestedActiveTabId }) => { return requestedId } - if (nextTabs.some(tab => tab?.id === 'component')) { - return 'component' + const activeTab = nextTabs.find(tab => tab?.isActive === true) + if (toNonEmptyWorkspaceText(activeTab?.id)) { + return toNonEmptyWorkspaceText(activeTab.id) + } + + const entryTab = nextTabs.find(tab => tab?.role === 'entry') + if (toNonEmptyWorkspaceText(entryTab?.id)) { + return toNonEmptyWorkspaceText(entryTab.id) } return toNonEmptyWorkspaceText(nextTabs[0]?.id) diff --git a/src/modules/workspace/workspace-tab-shape.js b/src/modules/workspace/workspace-tab-shape.js index 046c7d9..6b41c5b 100644 --- a/src/modules/workspace/workspace-tab-shape.js +++ b/src/modules/workspace/workspace-tab-shape.js @@ -2,8 +2,6 @@ const createEnsureWorkspaceTabsShape = ({ defaultComponentTabName, defaultComponentTabPath, - defaultStylesTabName, - defaultStylesTabPath, defaultJsx, normalizeEntryTabPath, getPathFileName, @@ -17,12 +15,12 @@ const createEnsureWorkspaceTabsShape = }) => tabs => { const inputTabs = Array.isArray(tabs) ? tabs : [] - const hasComponent = inputTabs.some(tab => tab?.id === 'component') + const hasEntryTab = inputTabs.some(tab => tab?.role === 'entry') const nextTabs = [...inputTabs] - if (!hasComponent) { + if (!hasEntryTab) { nextTabs.unshift({ - id: 'component', + id: 'entry', name: defaultComponentTabName, path: defaultComponentTabPath, language: 'javascript-jsx', @@ -33,7 +31,7 @@ const createEnsureWorkspaceTabsShape = } return nextTabs.map(tab => { - if (tab?.id === 'component') { + if (tab?.role === 'entry') { const normalizedEntryPath = normalizeEntryTabPath(tab.path, { preferredFileName: tab.name, }) @@ -56,44 +54,24 @@ const createEnsureWorkspaceTabsShape = } } - if (tab?.id === 'styles') { - const normalizedStylesPath = - toNonEmptyWorkspaceText(tab.path) || defaultStylesTabPath - const normalizedStylesNameInput = toNonEmptyWorkspaceText(tab.name) - const normalizedStylesTargetPath = - normalizeWorkspacePathValue(normalizedStylesPath) - return { - ...tab, - language: isStyleTabLanguage(tab.language) ? tab.language : 'css', - role: 'module', - content: typeof tab?.content === 'string' ? tab.content : '', - path: normalizedStylesPath, - name: - !normalizedStylesNameInput || - normalizedStylesNameInput.toLowerCase() === 'styles' - ? getPathFileName(normalizedStylesPath) || defaultStylesTabName - : normalizedStylesNameInput, - targetPrFilePath: normalizedStylesTargetPath || null, - isDirty: Boolean(tab?.isDirty), - syncedAt: toWorkspaceSyncTimestamp(tab?.syncedAt), - lastSyncedRemoteSha: toWorkspaceSyncSha(tab?.lastSyncedRemoteSha), - syncedContent: resolveSyncedBaselineContent({ - tab, - content: typeof tab?.content === 'string' ? tab.content : '', - }), - } - } - const nextPath = toNonEmptyWorkspaceText(tab?.path) + const normalizedModulePath = nextPath const normalizedModuleTargetPath = normalizeWorkspacePathValue(nextPath) const nextContent = typeof tab?.content === 'string' ? tab.content : '' + const normalizedNameInput = toNonEmptyWorkspaceText(tab?.name) + const normalizedLanguage = isStyleTabLanguage(tab?.language) + ? tab.language + : 'javascript-jsx' return { ...tab, role: 'module', - language: isStyleTabLanguage(tab?.language) ? tab.language : 'javascript-jsx', - path: nextPath, + language: normalizedLanguage, + path: normalizedModulePath, content: nextContent, - name: toNonEmptyWorkspaceText(tab?.name) || getPathFileName(nextPath) || tab?.id, + name: + normalizedNameInput || + getPathFileName(normalizedModulePath) || + toNonEmptyWorkspaceText(tab?.id), targetPrFilePath: normalizedModuleTargetPath || getTabTargetPrFilePath(tab) || null, isDirty: Boolean(tab?.isDirty), diff --git a/src/modules/workspace/workspace-tabs-state.js b/src/modules/workspace/workspace-tabs-state.js index d16ad0e..7d3d6d5 100644 --- a/src/modules/workspace/workspace-tabs-state.js +++ b/src/modules/workspace/workspace-tabs-state.js @@ -108,7 +108,12 @@ export const createWorkspaceTabsState = ({ tabs = [], activeTabId, onChange } = } if (orderedIds.length === 0) { - const fallback = normalizeTab({ id: 'component', name: 'Component' }) + const fallback = normalizeTab({ + id: 'entry', + name: 'Entry', + role: 'entry', + language: 'javascript-jsx', + }) tabsById.set(fallback.id, fallback) orderedIds.push(fallback.id) }