Skip to content

Commit

Permalink
feat: add code transformer to <Demo> component (#7894)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkajtoch authored Jul 19, 2024
1 parent e35371e commit e738c32
Show file tree
Hide file tree
Showing 6 changed files with 1,213 additions and 16 deletions.
36 changes: 36 additions & 0 deletions packages/docusaurus-theme/src/components/demo/code_transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const IMPORT_REGEX = /^import [^'"]* from ['"]([^.'"\n ][^'"\n ]*)['"];?/gm;
const DEFAULT_EXPORT_REGEX = /export default /;
const COMPONENT_ONLY_REGEX = /^\(?</;

/**
* Transforms input JS/TS source code to a react-live compatible syntax.
* react-live uses the surcase library to transform input source code into
* browser-readable JavaScript.
*
* While surcase does support CommonJS and ESM import/export statements,
* it's not trivial to expose our internal React and EUI exports through it
* and because we already control the execution scope of the interactive demos
* it isn't really necessary to implement a smart `require()` replacement.
*
* Returning an IIFE is necessary when the source code is more than just
* a JSX component definition (e.g. it contains a variable definition
* or `export default` statement).
*
* @see {@link https://github.com/alangpierce/sucrase}
* @see {@link https://github.com/FormidableLabs/react-live/blob/master/packages/react-live/src/utils/transpile/index.ts}
*/
export const demoCodeTransformer = (code: string) => {
// Remove ESM imports
code = code.replace(IMPORT_REGEX, '');

// Handle ESM default exports
code = code.replace(DEFAULT_EXPORT_REGEX, 'return ');

// If the demo is JSX only return as-is
if (COMPONENT_ONLY_REGEX.test(code)) {
return code;
}

// If the demo is more than just JSX wrap in an immediately invoked function expression
return `(() => { ${code} })()`;
}
48 changes: 38 additions & 10 deletions packages/docusaurus-theme/src/components/demo/demo.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { Children, PropsWithChildren, useCallback, useState } from 'react';
import {
Children,
PropsWithChildren,
useCallback,
useMemo,
useState,
} from 'react';
import { isElement } from 'react-is';
import { LiveProvider } from 'react-live';
import { themes as prismThemes } from 'prism-react-renderer';
import { useEuiMemoizedStyles, copyToClipboard, UseEuiTheme } from '@elastic/eui';
import {
useEuiMemoizedStyles,
copyToClipboard,
UseEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { DemoContext, DemoContextObject } from './context';
import { DemoEditor } from './editor';
import { DemoPreview } from './preview';
import { DemoSource } from './source';
import { demoScope } from './scope';
import { demoDefaultScope } from './scope';
import { DemoActionsBar } from './actions_bar';
import { demoCodeTransformer } from './code_transformer';

export interface DemoSourceMeta {
code: string;
Expand All @@ -18,11 +29,19 @@ export interface DemoSourceMeta {
}

export interface DemoProps extends PropsWithChildren {
/**
* Whether the source code editor is open by default
*/
isSourceOpen?: boolean;
/**
* Allows to extend the default scope of the rendered demo and pass additional
* properties available within the demo.
*
* The default scope exposes all React and EUI exports.
*/
scope?: Record<string, unknown>;
}

const transformCode = (code: string) => code;

const getDemoStyles = (euiTheme: UseEuiTheme) => ({
demo: css`
--docs-demo-border-color: ${euiTheme.euiTheme.colors.lightShade};
Expand All @@ -36,6 +55,7 @@ const getDemoStyles = (euiTheme: UseEuiTheme) => ({

export const Demo = ({
children,
scope,
isSourceOpen: _isSourceOpen = true
}: DemoProps) => {
const styles = useEuiMemoizedStyles(getDemoStyles);
Expand All @@ -46,9 +66,17 @@ export const Demo = ({
// liveProviderKey restarts the demo to its initial state
const [liveProviderKey, setLiveProviderKey] = useState<number>(0);

const addSource = useCallback<DemoContextObject['addSource']>((source: DemoSourceMeta) => {
setSources((sources) => ([...sources, source]));
}, []);
const finalScope = useMemo(() => ({
...demoDefaultScope,
...scope,
}), [scope]);

const addSource = useCallback<DemoContextObject['addSource']>(
(source: DemoSourceMeta) => {
setSources((sources) => ([...sources, source]));
},
[],
);

const onClickCopyToClipboard = useCallback(() => {
copyToClipboard(activeSource?.code || '');
Expand All @@ -69,9 +97,9 @@ export const Demo = ({
<LiveProvider
key={liveProviderKey}
code={activeSource?.code || ''}
transformCode={transformCode}
transformCode={demoCodeTransformer}
theme={prismThemes.dracula}
scope={demoScope}
scope={finalScope}
>
<DemoPreview />
<DemoActionsBar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const getEditorStyles = () => ({
& .prism-code {
border-radius: 0 0 calc(var(--docs-demo-border-radius) - 1px) calc(var(--docs-demo-border-radius) - 1px);
max-height: 450px;
overflow: auto;
}
`,
error: css`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { css } from '@emotion/react';
import { LiveError, LivePreview } from 'react-live';
import { LivePreview } from 'react-live';
import BrowserOnly from '@docusaurus/BrowserOnly';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common';
Expand Down Expand Up @@ -30,9 +30,9 @@ export const DemoPreview = () => {
{() => (
<>
<ErrorBoundary fallback={(params: any) => <ErrorBoundaryErrorMessageFallback {...params} />}>
<EuiFlexGroup css={styles.previewWrapper} alignItems="center" justifyContent="center">
<div css={styles.previewWrapper}>
<LivePreview />
</EuiFlexGroup>
</div>
</ErrorBoundary>
</>
)}
Expand Down
5 changes: 4 additions & 1 deletion packages/docusaurus-theme/src/components/demo/scope.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from 'react';
import * as EUI from '@elastic/eui';

export const demoScope: Record<string, unknown> = {
export const demoDefaultScope: Record<string, unknown> = {
// React
React,
...React,

// EUI exports
...EUI,
};
Loading

0 comments on commit e738c32

Please sign in to comment.