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

A quick experiment to get allow React components in Markdown pages and throughout the site #610

Merged
merged 30 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b781e43
Add experimental react support within markdown
chiedo Oct 16, 2020
797adb1
Add a cool table component
chiedo Oct 18, 2020
c5a34e4
Set up foundations for code switcher
chiedo Oct 19, 2020
d264c65
Add better code block component
chiedo Oct 19, 2020
d51688b
Everything working but CodeEditor
chiedo Oct 19, 2020
35c38e5
Add CodeEditor example
chiedo Oct 19, 2020
b1fa93f
Move object hash to dependencies
chiedo Oct 19, 2020
548853f
Add some comments to explain what the code in server does
chiedo Oct 20, 2020
6e5673c
Make code a little less sloppy
chiedo Oct 20, 2020
8ef2822
Add performance optimization
chiedo Oct 20, 2020
89d745b
Remove uneeded code
chiedo Oct 20, 2020
3c71bc7
Minor patches
chiedo Oct 20, 2020
b8c8e0f
Minor performance improvement
chiedo Oct 20, 2020
7573ff2
Add some comments that explain logic
chiedo Oct 20, 2020
fe81204
Make the rendering engine safe
chiedo Oct 21, 2020
a48998c
Keep all the React Engine logic in one file
chiedo Oct 21, 2020
561bd33
Update access-permissions-on-github.md
heiskr Dec 2, 2020
74d43b5
Reset a few more things
heiskr Dec 2, 2020
c084360
Update index.js
heiskr Dec 2, 2020
0c3309d
Reset package.json and package-lock.json
heiskr Dec 2, 2020
07a7822
Merge branch 'main' into experiment-with-react-and-mdx
heiskr Dec 2, 2020
d830284
Readd new packages
heiskr Dec 2, 2020
84473cb
Update engine.js
heiskr Dec 2, 2020
791c7c6
Run lint
heiskr Dec 2, 2020
8731b68
Update engine.js
heiskr Dec 2, 2020
632ef6c
Update check-deps.js
heiskr Dec 2, 2020
2d07086
Add documentation for using React components
chiedo Dec 3, 2020
f89c983
Update README.md
heiskr Dec 3, 2020
92ba8d9
Merge branch 'main' into experiment-with-react-and-mdx
heiskr Dec 3, 2020
c410020
Update engine.js
heiskr Dec 3, 2020
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
3 changes: 3 additions & 0 deletions lib/frontmatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ const schema = {
href: { type: 'string' }
}
}
},
interactive: {
type: 'boolean'
}
}
}
Expand Down
24 changes: 23 additions & 1 deletion lib/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const pathUtils = require('./path-utils')
const Permalink = require('./permalink')
const languages = require('./languages')
const renderContent = require('./render-content')
const { renderReact } = require('./react/engine')
const frontmatter = require('./frontmatter')
const products = require('./all-products')
const slash = require('slash')
Expand Down Expand Up @@ -133,10 +134,31 @@ class Page {
this.title = await renderContent(this.rawTitle, context, { textOnly: true, encodeEntities: true })
this.shortTitle = await renderContent(this.shortTitle, context, { textOnly: true, encodeEntities: true })

const markdown = this.mapTopic
let markdown = this.mapTopic
? getMapTopicContent(this, context.pages, context.redirects)
: this.markdown

// If the article is interactive parse the React!
if (this.interactive) {
// Search for the react code comments to find the react components
const reactComponents = markdown.match(/<!--react-->(.*?)<!--end-react-->/gs)

// Render each of the react components in the markdown
chiedo marked this conversation as resolved.
Show resolved Hide resolved
await Promise.all(reactComponents.map(async (reactComponent) => {
let componentStr = reactComponent

// Remove the React comment indicators
componentStr = componentStr.replace('<!--react-->\n', '').replace('<!--react-->', '')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible to use a RegExp with replace, like

Suggested change
componentStr = componentStr.replace('<!--react-->\n', '').replace('<!--react-->', '')
componentStr = componentStr.replace(/<!--react-->\n?/g, '')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm this works @heiskr and if it does can you commit it? I get confused by the first replace having a new line character and the next one not. I personally like coding verbose. But that's a personal preference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its fine as is, just an FYI more than anything.

componentStr = componentStr.replace('\n<!--end-react-->', '').replace('<!--end-react-->', '')

// Get the rendered component
const renderedComponent = await renderReact(componentStr)

// Replace the react component with the rendered markdown
markdown = markdown.replace(reactComponent, renderedComponent)
}))
}

const html = await renderContent(markdown, context)

// product frontmatter may contain liquid
Expand Down
22 changes: 22 additions & 0 deletions lib/react/babel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const babel = require('@babel/core')

const reactBabelOptions = {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [
'@babel/plugin-transform-react-jsx',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
'@babel/transform-runtime'
]
}

const transform = code =>
babel.transform(code, reactBabelOptions).code

module.exports = {
transform: transform,
reactBabelOptions: reactBabelOptions
}
57 changes: 57 additions & 0 deletions lib/react/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { renderToString } = require('react-dom/server')
const { transform } = require('./babel')
const React = require('react')
const fs = require('fs')
const path = require('path')
const dirTree = require('directory-tree')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have a walk-sync module, but I don't know how different these are

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to try and switch to it @heiskr so we don't have to add another package?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like directory-tree and walk-sync have different output. walk-sync creates a flat array of just the file names, where directory-tree has a nested structure. I think there might be consequences to the difference as this code seems to look at just the first level. I think it's okay to ship directory-tree for now, and we can always change it later if we find another pattern we prefer.


// Name of directory for saving transformed components that should be gitignored
const dist = 'dist'

// Build React components
// This loops through the react components and transpiles them to /dist
// so they can be used by Node.js when we do server side rendering
const tree = dirTree('./react/')
if (tree) {
for (const index in tree.children) {
const file = tree.children[index]
if (file.type === 'file') {
if (!fs.existsSync(path.join(dist, 'react'))) {
fs.mkdirSync(path.join(dist, 'react'), { recursive: true })
}
const content = transform(fs.readFileSync(file.path, 'utf8'))
fs.writeFileSync(path.join(dist, file.path), content)
}
}
}
// End Build React Components

// Register components
const components = {
// CodeBlock: require('../../dist/react/CodeBlock'),
// CodeEditor: require('../../dist/react/CodeEditor')
}

const renderReact = async componentStr => {
// Get component name as string so we can use it in the class name
// which will be needed later if we choose to do client side React hydration
const componentName = componentStr.match(/<([a-zA-Z]+)\s/)[1]
// Add the wrapper and class name so we can later use React hydration on the client
// side
const jsx = `<div className="react-component-${componentName}">\n${componentStr}\n</div>`

const component = transform(jsx)

// eslint-disable-next-line
const getComponent = new Function(
'React',
...Object.keys(components),
`${component.replace('React', 'return React')}`
)

return renderToString(getComponent(React, ...Object.values(components)))
}

module.exports = {
renderReact
}
Loading