Skip to content

Commit

Permalink
[docs] WIP Live demos
Browse files Browse the repository at this point in the history
  • Loading branch information
mbrookes committed Jan 27, 2021
1 parent 9f3c45b commit c368013
Show file tree
Hide file tree
Showing 470 changed files with 7,749 additions and 261 deletions.
2 changes: 2 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"final-form": "^4.18.5",
"flexsearch": "^0.6.30",
"fs-extra": "^9.0.0",
"jarle": "^1.1.0",
"json2mq": "^0.2.0",
"jss": "^10.0.3",
"jss-plugin-template": "^10.0.3",
Expand All @@ -102,6 +103,7 @@
"react-redux": "^7.1.1",
"react-router": "^5.0.0",
"react-router-dom": "^5.0.1",
"react-simple-code-editor": "^0.11.0",
"react-spring": "^8.0.27",
"react-swipeable-views": "^0.13.9",
"react-text-mask": "^5.0.2",
Expand Down
197 changes: 197 additions & 0 deletions docs/scripts/createDemoImports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/* eslint-disable no-console */
/**
* Create live demo imports.
*/

/**
* List of demos to ignore when transpiling
* Example: ['app-bar/BottomAppBar.tsx']
*/
const ignoreList = ['components/.eslintrc.js', 'material-icons/synonyms.js'];

const fse = require('fs-extra');
const path = require('path');
const prettier = require('prettier');
const yargs = require('yargs');
const { fixLineEndings } = require('./helpers');

const workspaceRoot = path.join(__dirname, '../../');

async function getFiles(root) {
const files = [];

await Promise.all(
(await fse.readdir(root)).map(async (name) => {
const filePath = path.join(root, name);
const stat = await fse.stat(filePath);

if (stat.isDirectory()) {
files.push(...(await getFiles(filePath)));
} else if (
stat.isFile() &&
/docs\/src\/pages\/components\/.*\.js?$/.test(filePath) &&
!filePath.endsWith('.tsx') &&
!filePath.endsWith('Imports.js') &&
!ignoreList.some((ignorePath) => filePath.endsWith(path.normalize(ignorePath)))
) {
files.push(filePath);
}
}),
);

return files;
}

const TranspileResult = {
Success: 0,
Failed: 1,
};

async function transpileFile(tsxPath) {
const jsPath = tsxPath.replace(/\.tsx?$/, '.js');
try {
let source = await fse.readFile(jsPath, 'utf8');

// Flatten multiline imports
source = source
.replace(/{\n/gm, '{')
.replace(/,\n/gm, ',')
.replace(/,}/gm, ' }')
.replace(/ {2}/gm, ' ');

// Extract imports
const importsRegex = /^import.*$/gm;
const imports = source.match(importsRegex);

const importRegex = /^import (.*) from (.*);$/;
const asRegex = /\w+ as\s*/gm;
const firstOrLastRegex = /(^\w+|\w+$)/;
const inBracesRegex = /({.+})/;

// Prepare imports (object as string) for Jarle resolveImports
const transformedImports = imports.reduce((accumulator, currentImport) => {
const splitImport = currentImport.match(importRegex);

// Remove '\w+ as': { red as blue } => { red }
const module = splitImport[1].replace(asRegex, '');

const defaultExport = module.match(firstOrLastRegex);
const namedExports = module.match(inBracesRegex);

// 'red' || '{ blue }
let modules = defaultExport ? defaultExport[1] : namedExports[1];

// 'red, { blue }' => '{ default: red, blue }
if (defaultExport && namedExports) {
modules = `${namedExports[1].slice(0, 1)} default: ${
defaultExport[1]
},${namedExports[1].slice(1)}`;
}

const newImport =
splitImport[2] === `'${modules}'` ? modules : `${splitImport[2]}: ${modules}`;
return `${accumulator}\n${newImport},`;
}, '{\n');

const output = `${imports.join('\n')};
export default ${transformedImports}};`;

const prettierConfig = prettier.resolveConfig.sync(jsPath, {
config: path.join(workspaceRoot, 'prettier.config.js'),
});
const prettierFormat = (jsSource) =>
prettier.format(jsSource, { ...prettierConfig, filepath: jsPath });

const prettified = prettierFormat(output);
const correctedLineEndings = fixLineEndings(source, prettified);

await fse.writeFile(jsPath.replace(/\.js?$/, 'Imports.js'), correctedLineEndings);
return TranspileResult.Success;
} catch (err) {
console.error('Something went wrong with %s\n%s\n', tsxPath, err);
return TranspileResult.Failed;
}
}

async function main(argv) {
const { watch: watchMode, pattern } = argv;

const filePattern = new RegExp(pattern);
if (pattern.length > 0) {
console.log(`Only considering demos matching ${filePattern}`);
}

const files = (await getFiles(path.join(workspaceRoot, 'docs/src/pages'))).filter((fileName) => {
return filePattern.test(fileName);
});

let successful = 0;
let failed = 0;
(await Promise.all(files.map((file) => transpileFile(file)))).forEach((result) => {
switch (result) {
case TranspileResult.Success: {
successful += 1;
break;
}
case TranspileResult.Failed: {
failed += 1;
break;
}
default: {
throw new Error(`No handler for ${result}`);
}
}
});

console.log(
[
'------ Summary ------',
'%i demo(s) were successfully converted',
'%i demo(s) were unsuccessful',
].join('\n'),
successful,
failed,
);

if (!watchMode) {
if (failed > 0) {
process.exit(1);
}
return;
}

files.forEach((filePath) => {
fse.watchFile(filePath, { interval: 500 }, async () => {
if ((await transpileFile(filePath, program, true)) === 0) {
console.log('Success - %s', filePath);
}
});
});

console.log('\nWatching for file changes...');
}

yargs
.command({
command: '$0',
description: 'Convert demos',
builder: (command) => {
return command
.option('watch', {
default: false,
description: 'Convert demos as soon as they changed',
type: 'boolean',
})
.option('pattern', {
default: '',
description: 'Convert only the JS demos whose filename matches the given pattern.',
type: 'string',
});
},
handler: main,
})
.help()
.strict(true)
.version(false)
.parse();
80 changes: 34 additions & 46 deletions docs/src/modules/components/Demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { useSelector } from 'react-redux';
import { Error, Preview, Provider } from 'jarle';
import { alpha, makeStyles } from '@material-ui/core/styles';
import IconButton from '@material-ui/core/IconButton';
import Collapse from '@material-ui/core/Collapse';
import NoSsr from '@material-ui/core/NoSsr';
import HighlightedCode from 'docs/src/modules/components/HighlightedCode';
import DemoSandboxed from 'docs/src/modules/components/DemoSandboxed';
import { AdCarbonInline } from 'docs/src/modules/components/AdCarbon';
import getJsxPreview from 'docs/src/modules/utils/getJsxPreview';
import { CODE_VARIANTS } from 'docs/src/modules/constants';
import { useUserLanguage, useTranslate } from 'docs/src/modules/utils/i18n';
import DemoEditor from './DemoEditor';

const DemoToolbar = React.lazy(() => import('./DemoToolbar'));

// Sync with styles from DemoToolbar
// Importing the styles results in no bundle size reduction
const useDemoToolbarFallbackStyles = makeStyles(
Expand All @@ -23,13 +24,14 @@ const useDemoToolbarFallbackStyles = makeStyles(
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'flex',
height: theme.spacing(6),
height: 48,
},
},
};
},
{ name: 'DemoToolbar' },
);

export function DemoToolbarFallback() {
const classes = useDemoToolbarFallbackStyles();
const t = useTranslate();
Expand All @@ -46,24 +48,24 @@ function getDemoName(location) {
function useDemoData(codeVariant, demo, githubLocation) {
const userLanguage = useUserLanguage();
const title = `${getDemoName(githubLocation)} Material Demo`;

if (codeVariant === CODE_VARIANTS.TS && demo.rawTS) {
return {
codeVariant: CODE_VARIANTS.TS,
githubLocation: githubLocation.replace(/\.js$/, '.tsx'),
language: userLanguage,
raw: demo.rawTS,
Component: demo.tsx,
imports: demo.imports,
sourceLanguage: 'tsx',
title,
};
}

return {
codeVariant: CODE_VARIANTS.JS,
githubLocation,
language: userLanguage,
raw: demo.raw,
Component: demo.js,
imports: demo.imports,
sourceLanguage: 'jsx',
title,
};
Expand Down Expand Up @@ -131,21 +133,6 @@ const useStyles = makeStyles(
paddingTop: theme.spacing(3),
},
},
code: {
display: 'none',
padding: 0,
marginBottom: theme.spacing(1),
marginRight: 0,
[theme.breakpoints.up('sm')]: {
display: 'block',
},
'& pre': {
overflow: 'auto',
lineHeight: 1.5,
margin: '0 !important',
maxHeight: 'min(68vh, 1000px)',
},
},
anchorLink: {
marginTop: -64, // height of toolbar
position: 'absolute',
Expand Down Expand Up @@ -174,15 +161,7 @@ export default function Demo(props) {
setDemoHovered(event.type === 'mouseenter');
};

const DemoComponent = demoData.Component;
const demoName = getDemoName(demoData.githubLocation);
const demoSandboxedStyle = React.useMemo(
() => ({
maxWidth: demoOptions.maxWidth,
height: demoOptions.height,
}),
[demoOptions.height, demoOptions.maxWidth],
);

if (demoOptions.bg == null) {
demoOptions.bg = 'outlined';
Expand Down Expand Up @@ -212,8 +191,24 @@ export default function Demo(props) {
jsx !== demoData.raw &&
jsx.split(/\n/).length <= 17;

const [editorValue, setEditorValue] = React.useState(demoData.raw.trim());
const handleEditorValueChange = (value) => {
setEditorValue(value);
};

const handleEditorFocus = () => {
setCodeOpen(true);
};

const [demoKey, resetDemo] = React.useReducer((key) => key + 1, 0);

const handleResetDemo = () => {
resetDemo();
setEditorValue(demoData.raw);
};

const resolveImports = () => demoData.imports;

const demoId = useUniqueId('demo-');
const demoSourceId = useUniqueId(`demoSource-`);
const openDemoSource = codeOpen || showPreview;
Expand Down Expand Up @@ -241,14 +236,10 @@ export default function Demo(props) {
action={initialFocusRef}
tabIndex={-1}
/>
<DemoSandboxed
key={demoKey}
style={demoSandboxedStyle}
component={DemoComponent}
iframe={demoOptions.iframe}
name={demoName}
onResetDemoClick={resetDemo}
/>
<Provider code={editorValue} resolveImports={resolveImports}>
<Preview key={demoKey} />
<Error />
</Provider>
</div>
<div className={classes.anchorLink} id={`${demoName}.js`} />
<div className={classes.anchorLink} id={`${demoName}.tsx`} />
Expand All @@ -270,22 +261,19 @@ export default function Demo(props) {
setCodeOpen((open) => !open);
setShowAd(true);
}}
onResetDemoClick={resetDemo}
onResetDemoClick={handleResetDemo}
openDemoSource={openDemoSource}
showPreview={showPreview}
/>
</React.Suspense>
</NoSsr>
)}
<Collapse in={openDemoSource} unmountOnExit>
<div>
<HighlightedCode
className={classes.code}
id={demoSourceId}
code={showPreview && !codeOpen ? jsx : demoData.raw}
language={demoData.sourceLanguage}
/>
</div>
<DemoEditor
value={showPreview && !codeOpen ? jsx : editorValue}
onValueChange={handleEditorValueChange}
onFocus={handleEditorFocus}
/>
</Collapse>
{showAd && !disableAd && !demoOptions.disableAd ? <AdCarbonInline /> : null}
</div>
Expand Down
Loading

0 comments on commit c368013

Please sign in to comment.