Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(site-demo): add codesandbox URL #1029

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/site-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@mdx-js/react": "^1.6.19",
"@reach/router": "^1.2.1",
"classnames": "^2.3.2",
"codesandbox-import-utils": "^2.2.3",
"focus-visible": "^5.2.0",
"gatsby": "^2.24.87",
"gatsby-image": "^2.4.21",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const fs = require('fs');
const path = require('path');
const memoize = require('lodash/memoize');
const { ROOT_PATH } = require('../../../../configs/path');
const { getParameters } = require('codesandbox-import-utils/lib/api/define');

const REACT_VERSION = '18.2.0';

/** Base config for the LumX CodeSandbox */
const BASE_SANDBOX_CONFIG = {
// Bootstrap app with LumX core and ReactDOM.createRoot
'index.tsx': {
content: `import "@lumx/core/lumx.css";
import ReactDOM from "react-dom/client";
import { App } from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(<App />);`,
},
'package.json': {
content: JSON.stringify({
main: 'index.tsx',
dependencies: {
'@lumx/core': 'latest',
'@lumx/icons': 'latest',
'@lumx/react': 'latest',
react: REACT_VERSION,
'react-dom': REACT_VERSION,
},
devDependencies: {
'@types/react': REACT_VERSION,
'@types/react-dom': REACT_VERSION,
typescript: '4.4.2',
},
}),
},
};

// Extra demo components we do not ship in @lumx
const DEMO_COMPONENTS = {
'@lumx/demo/components/content/Placeholder': {
source: 'packages/site-demo/src/components/content/Placeholder/index.tsx',
fileName: 'Placeholder.tsx',
},
};

/** Prepare files to load in the sandbox based on the demo code */
function prepareFiles(pageImports, demoCode) {
const config = { ...BASE_SANDBOX_CONFIG };
let content = demoCode;

// JSX without imports or components
if (content.trim().startsWith('<')) {
// Wrap JSX code into a component
const component = `export const App = () => (
<>
${content.replaceAll('\n', '\n ')}
</>
);`;

// Add page imports and then the component
content = `${pageImports}\n\n${component}`;
}

// Replace path to demo components
for (let [originalPath, { source, fileName }] of Object.entries(DEMO_COMPONENTS)) {
if (content.includes(originalPath)) {
// Replace path (without extension)
content = content.replaceAll(originalPath, `./${fileName.replace('\..+$')}`);
// Put demo component file in the sandbox
const demoFileContent = fs.readFileSync(path.resolve(ROOT_PATH, source)).toString();
config[fileName] = { content: demoFileContent };
}
}

// Add demo code in the sandbox
config['App.tsx'] = { content };

return config;
}

/** List import code lines from the MDX page */
const getPageImports = memoize((mdxString) => {
// Make sure to always have React
const lines = new Set(["import React from 'react';"]);
for (const line of mdxString.trim().split('\n')) {
let lineTrimmed = line.trim();
if (!lineTrimmed || !lineTrimmed.startsWith('import')) break;
lines.add(lineTrimmed);
}
return Array.from(lines.values()).join('\n');
});

/**
* Try to generate a CodeSandbox URL for the given demo code
*/
module.exports = function generateCodesandboxURL(mdxString, demoCode) {
// Get JS import from the page
const pageImports = getPageImports(mdxString);
// Generate sandbox files
const files = prepareFiles(pageImports, demoCode);
// Generate CodeSandbox URL parameters
const parameters = getParameters({ files });
return `https://codesandbox.io/api/v1/sandboxes/define?parameters=${parameters}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const fs = require('fs');
const CONTENT_DIR = path.resolve('./content');
const rewriteJSXComponents = require('../utils/rewriteJSXComponents');
const debug = require('../utils/debug');
const generateCodesandboxURL = require('./generate-codesandbox')

/** Read source code to string (or return null if source code not found). */
async function readSourceCode(sourcePath) {
Expand All @@ -25,11 +26,13 @@ const removeIndent = (code) => {
}

/** Update <DemoBlock/> props to import source code. */
async function updateDemoBlock(resourceFolder, addImport, props) {
async function updateDemoBlock(resourceFolder, addImport, mdxString, props) {
if (props.children) {
// <DemoBlock> with children already have a demo inside them.
// We copy the demo as string into the `codeString` prop.
props.codeString = JSON.stringify(removeIndent(props.children));
let code = removeIndent(props.children);
props.codeString = JSON.stringify(code);
props.codesandboxURL = JSON.stringify(generateCodesandboxURL(mdxString, code))
return props;
}

Expand All @@ -48,6 +51,7 @@ async function updateDemoBlock(resourceFolder, addImport, props) {
return props;
}
props.codeString = JSON.stringify(code.trim());
props.codesandboxURL = JSON.stringify(generateCodesandboxURL(mdxString, code.trim()))

// Import demo (will be added at the top).
let relativePath = path.relative(CONTENT_DIR, sourcePath);
Expand All @@ -73,7 +77,7 @@ module.exports = async (filePath, mdxString) => {
mdxString = await rewriteJSXComponents(
'DemoBlock',
mdxString,
partial(updateDemoBlock, resourceFolder, addImport)
partial(updateDemoBlock, resourceFolder, addImport, mdxString)
);

if (imports.length) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
========================================================================== */

.code-block {
position: relative;
display: block;
padding: $lumx-spacing-unit * 2;
padding: $lumx-spacing-unit-big;
overflow-x: auto;
font-size: 14px;
color: #333;
Expand All @@ -34,4 +35,10 @@
font-weight: inherit;
background: none;
}

& &__edit-on-codesandbox {
position: absolute;
top: $lumx-spacing-unit-big;
right: $lumx-spacing-unit-big;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import classNames from 'classnames';
import isString from 'lodash/isString';
import Highlight, { defaultProps, Language } from 'prism-react-renderer';
import React from 'react';
import { IconButton } from '@lumx/react';
import { mdiSquareEditOutline } from '@lumx/icons';
import { theme } from './init-prism';
import { renderJSXLinesWithCollapsedImports } from './renderJSXLinesWithCollapsedImports';
import { renderLines } from './renderLines';
Expand All @@ -15,10 +17,18 @@ interface Props {
codeString?: string;
/** Code language (tsx, jsx, etc.) */
language?: Language | 'tsx';
/** URL to the CodeSandbox. */
codesandboxURL?: string;
}

/** Display syntax highlighted code */
export const CodeBlock: React.FC<Props> = ({ className, codeString, language: propLanguage, children }) => {
export const CodeBlock: React.FC<Props> = ({
className,
codeString,
language: propLanguage,
children,
codesandboxURL,
}) => {
const language = propLanguage || className?.match(/language-(\w+)/)?.[1];
if (!language) {
return <pre className={classNames('code-block', className)}>{children || codeString}</pre>;
Expand All @@ -29,6 +39,16 @@ export const CodeBlock: React.FC<Props> = ({ className, codeString, language: pr
<Highlight {...defaultProps} theme={theme} code={code} language={language as Language}>
{({ className: prismClassName, ...renderParams }) => (
<pre className={classNames('code-block', prismClassName, className)}>
{codesandboxURL && (
<IconButton
className="code-block__edit-on-codesandbox"
icon={mdiSquareEditOutline}
emphasis="low"
label="Edit on CodeSandbox"
target="_blank"
href={codesandboxURL}
/>
)}
{language === 'jsx' || language === 'tsx'
? renderJSXLinesWithCollapsedImports(renderParams)
: renderLines(renderParams)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import './DemoBlock.scss';
interface DemoBlockProps extends FlexBoxProps {
demo?: string;
codeString?: string;
codesandboxURL?: string;
withThemeSwitcher?: boolean;
hasPlayButton?: boolean;
backgroundColor?: { color: ColorPalette; variant: ColorVariant };
Expand All @@ -37,6 +38,7 @@ export const DemoBlock: React.FC<DemoBlockProps> = ({
children,
demo,
codeString,
codesandboxURL,
withThemeSwitcher = false,
hasPlayButton = false,
backgroundColor: propBackgroundColor,
Expand Down Expand Up @@ -103,6 +105,7 @@ export const DemoBlock: React.FC<DemoBlockProps> = ({
<CodeBlock
className={classNames('demo-block__code', showCode && codeString && 'demo-block__code--shown')}
codeString={codeString}
codesandboxURL={codesandboxURL}
language="tsx"
/>
</div>
Expand Down
Loading