Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 67 additions & 39 deletions packages/toolbar/src/core/mount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,39 @@ export default function mount(rootNode: HTMLElement, config: InitializationConfi
};
}

const { host, reactMount, observer } = buildDom();
const { host, reactMount, observer, cleanupSnapshot } = buildDom();

const reactRoot = createRoot(reactMount);

// Dynamically import toolbar to capture style injection timing
import('./ui/Toolbar/LaunchDarklyToolbar').then((module) => {
const { LaunchDarklyToolbar } = module;
reactRoot.render(
<StrictMode>
<LaunchDarklyToolbar
domId={TOOLBAR_DOM_ID}
baseUrl={config.baseUrl}
devServerUrl={config.devServerUrl}
projectKey={config.projectKey}
flagOverridePlugin={config.flagOverridePlugin}
eventInterceptionPlugin={config.eventInterceptionPlugin}
pollIntervalInMs={config.pollIntervalInMs}
position={config.position}
/>
</StrictMode>,
);
});
import('./ui/Toolbar/LaunchDarklyToolbar')
.then((module) => {
const { LaunchDarklyToolbar } = module;
reactRoot.render(
<StrictMode>
<LaunchDarklyToolbar
domId={TOOLBAR_DOM_ID}
baseUrl={config.baseUrl}
devServerUrl={config.devServerUrl}
projectKey={config.projectKey}
flagOverridePlugin={config.flagOverridePlugin}
eventInterceptionPlugin={config.eventInterceptionPlugin}
pollIntervalInMs={config.pollIntervalInMs}
position={config.position}
/>
</StrictMode>,
);
// Disconnect observer after toolbar is loaded to prevent unnecessary monitoring
observer.disconnect();
// Clean up snapshot to free memory
cleanupSnapshot();
})
.catch((error) => {
console.error('[LaunchDarkly Toolbar] Failed to load toolbar:', error);
observer.disconnect();
// Clean up snapshot even on error
cleanupSnapshot();
});

cleanup.push(() => {
observer.disconnect();
Expand All @@ -61,30 +72,46 @@ function buildDom() {
host.style.zIndex = '2147400100';

const shadowRoot = host.attachShadow({ mode: 'open' });
if (!shadowRoot) {
throw new Error('[LaunchDarkly Toolbar] Failed to create shadow root');
}

const reactMount = document.createElement('div');

// Snapshot existing styles BEFORE the toolbar component loads
const existingStylesSnapshot = new Set(
Array.from(document.head.querySelectorAll('style')).map((el) => el.textContent || ''),
);
let existingStylesSnapshot: Set<string> | null = new Set();
if (document.head) {
const headStyles = document.head.querySelectorAll('style');
existingStylesSnapshot = new Set(Array.from(headStyles).map((el) => el.textContent || ''));
}

// Copy existing LaunchPad styles (including Gonfalon's) to shadow root
// so toolbar has the base styles it needs
const existingStyles = Array.from(document.head.querySelectorAll('style'))
.filter((styleEl) => styleEl.textContent?.includes('--lp-') || styleEl.textContent?.includes('_'))
.map((styleEl) => styleEl.textContent || '')
.join('\n');

if (existingStyles) {
const style = document.createElement('style');
style.textContent = existingStyles;
shadowRoot.appendChild(style);
if (document.head) {
const existingStyles = Array.from(document.head.querySelectorAll('style'))
.filter((styleEl) => styleEl.textContent?.includes('--lp-') || styleEl.textContent?.includes('_'))
.map((styleEl) => styleEl.textContent || '')
.join('\n');

if (existingStyles) {
const style = document.createElement('style');
style.textContent = existingStyles;
shadowRoot.appendChild(style);
}
}

reactMount.dataset.name = 'react-mount';
reactMount.id = 'ld-toolbar-react-mount';
shadowRoot.appendChild(reactMount);

// Cleanup function to free memory
const cleanupSnapshot = () => {
if (existingStylesSnapshot) {
existingStylesSnapshot.clear();
existingStylesSnapshot = null;
}
};

// Watch for NEW styles injected by the toolbar and redirect them to shadow root
// This prevents toolbar's LaunchPad styles from overriding host app custom styles
const observer = new MutationObserver((mutations) => {
Expand All @@ -95,8 +122,9 @@ function buildDom() {
const content = styleEl.textContent || '';

// Check if this is a NEW LaunchPad/toolbar style (not from host app)
const isNewToolbarStyle =
!existingStylesSnapshot.has(content) && (content.includes('--lp-') || content.includes('_'));
const isNewStyle = existingStylesSnapshot ? !existingStylesSnapshot.has(content) : true;
const isToolbarStyle = content.includes('--lp-') || content.includes('_');
const isNewToolbarStyle = isNewStyle && isToolbarStyle;

if (isNewToolbarStyle) {
// Copy to shadow root so toolbar still works
Expand All @@ -108,19 +136,19 @@ function buildDom() {
// We can remove immediately since we've already copied to shadow root
try {
styleEl.remove();
} catch {
// Ignore if already removed
} catch (error) {
console.warn('[LaunchDarkly Toolbar] Failed to remove style element from document.head:', error);
}
}
}
});
});
});

observer.observe(document.head, { childList: true });

// Stop observing after 500ms (toolbar should be fully loaded by then)
setTimeout(() => observer.disconnect(), 500);
// Only observe document.head if it exists
if (document.head) {
observer.observe(document.head, { childList: true });
}

return { host, reactMount, observer };
return { host, reactMount, observer, cleanupSnapshot };
}
Loading
Loading