diff --git a/package.json b/package.json index 56ba793821cd..be2ecf9d3efa 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "clean-webpack-plugin": "^0.1.17", "copy-webpack-plugin": "^4.3.0", "css-loader": "^0.28.5", - "directory-tree-webpack-plugin": "^0.2.0", + "directory-tree-webpack-plugin": "^0.3.1", "duplexer": "^0.1.1", "eslint": "4.5.0", "eslint-loader": "^1.9.0", diff --git a/src/components/Page/Page.jsx b/src/components/Page/Page.jsx index bbbc13d7288a..657d7c4c55ca 100644 --- a/src/components/Page/Page.jsx +++ b/src/components/Page/Page.jsx @@ -11,8 +11,28 @@ import Gitter from '../Gitter/Gitter'; import './Page.scss'; class Page extends React.Component { - state = { - content: this.props.content instanceof Promise ? 'Loading...' : this.props.content + constructor(props) { + super(props); + + const { content } = props; + + this.state = { + content: content instanceof Promise ? 'Loading...' : content.default || content + }; + } + + componentDidMount() { + const { content } = this.props; + + if (content instanceof Promise) { + content + .then(module => this.setState({ + content: module.default || module + })) + .catch(error => this.setState({ + content: 'Error loading content.' + })); + } } render() { @@ -66,18 +86,6 @@ class Page extends React.Component { ); } - - componentDidMount() { - if ( this.props.content instanceof Promise ) { - this.props.content - .then(module => this.setState({ - content: module.default || module - })) - .catch(error => this.setState({ - content: 'Error loading content.' - })); - } - } } export default Page; diff --git a/src/components/Site/Site.jsx b/src/components/Site/Site.jsx index ea37bec2fe37..290e5bf6509f 100644 --- a/src/components/Site/Site.jsx +++ b/src/components/Site/Site.jsx @@ -3,6 +3,9 @@ import React from 'react'; import { Switch, Route } from 'react-router-dom'; import { hot as Hot } from 'react-hot-loader'; +// Import Utilities +import { ExtractPages, ExtractSections } from '../../utilities/content-utils'; + // Import Components import NotificationBar from '../NotificationBar/NotificationBar'; import Navigation from '../Navigation/Navigation'; @@ -20,27 +23,8 @@ import '../../styles/index'; import '../../styles/icon.font.js'; import './Site.scss'; -// Load & Clean Up JSON Representation of `src/content` -// TODO: Consider moving all or parts of this cleaning to a `DirectoryTreePlugin` option(s) +// Load Content Tree import Content from '../../_content.json'; -Content.children = Content.children - .filter(item => item.name !== 'images') - .map(item => { - if ( item.type === 'directory' ) { - return { - ...item, - children: item.children.sort((a, b) => { - let group1 = (a.group || '').toLowerCase(); - let group2 = (b.group || '').toLowerCase(); - - if (group1 < group2) return -1; - if (group1 > group2) return 1; - return a.sort - b.sort; - }) - }; - - } else return item; - }); class Site extends React.Component { state = { @@ -50,8 +34,9 @@ class Site extends React.Component { render() { let { location } = this.props; let { mobileSidebarOpen } = this.state; - let sections = this._sections; + let sections = ExtractSections(Content); let section = sections.find(({ url }) => location.pathname.startsWith(url)); + let pages = ExtractPages(Content); return (
@@ -89,7 +74,7 @@ class Site extends React.Component { render={ props => ( - { this._pages.map(page => ( + { pages.map(page => ( { - return array.reduce((flat, item) => { - return flat.concat( - Array.isArray(item.children) ? this._flatten(item.children) : item - ); - }, []); - } - /** * Strip any non-applicable properties * @@ -173,28 +144,6 @@ class Site extends React.Component { children: children ? this._strip(children) : [] })); } - - /** - * Get top-level sections - * - * @return {array} - ... - */ - get _sections() { - return Content.children.filter(item => ( - item.type === 'directory' - )); - } - - /** - * Get all markdown pages - * - * @return {array} - ... - */ - get _pages() { - return this._flatten(Content.children).filter(item => { - return item.extension === '.md'; - }); - } } export default Hot(module)(Site); diff --git a/src/components/Splash/Splash.jsx b/src/components/Splash/Splash.jsx index fcdeefbba718..a3720b571c0c 100644 --- a/src/components/Splash/Splash.jsx +++ b/src/components/Splash/Splash.jsx @@ -7,8 +7,8 @@ import SplashViz from '../SplashViz/SplashViz'; import Markdown from '../Markdown/Markdown'; import Support from '../Support/Support'; -// Import Content -import Content from '../../content/index.md'; +// Import Demo Content +import SplashContent from '../../content/index.md'; // Load Styling import './Splash.scss'; @@ -21,7 +21,7 @@ const Splash = () => (
diff --git a/src/index.jsx b/src/index.jsx index fc205186f3a7..abdb860d91cd 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,22 +1,45 @@ +// Import External Dependencies import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Route } from 'react-router-dom'; + +// Import Components import Site from './components/Site/Site'; +// Import Utilities +import { FindInContent } from './utilities/content-utils'; + +// Import Content Tree +import Content from './_content.json'; + // TODO: Re-integrate // Consider `react-g-analytics` package +const render = process.NODE_ENV === 'production' ? ReactDOM.hydrate : ReactDOM.render; + // Client Side Rendering if ( window.document !== undefined ) { - ReactDOM.render(( - - ( - import(`./content/${path}`) } /> - )} /> - - ), document.getElementById('root')); + let { pathname } = window.location; + let trimmed = pathname.replace(/(.+)\/$/, '$1'); + let entryPage = FindInContent(Content, item => item.url === trimmed); + let entryPath = entryPage.path.replace('src/content/', ''); + + import(`./content/${entryPath}`).then(entryModule => { + render(( + + ( + { + if ( path === entryPath ) { + return entryModule.default || entryModule; + + } else return import(`./content/${path}`); + }} /> + )} /> + + ), document.getElementById('root')); + }); } diff --git a/src/server.jsx b/src/server.jsx index 582cc85ba114..035baa9e8b1d 100644 --- a/src/server.jsx +++ b/src/server.jsx @@ -16,11 +16,7 @@ const bundles = [ '/index.bundle.js' ]; -// Export method for `StaticSiteGeneratorPlugin` -// CONSIDER: How high can we mount `Site` into the DOM hierarchy? If -// we could start at ``, much of this could be moved to the `Site` -// component itself (allowing easier utilization of page data for title, -// description, etc). +// Export method for `SSGPlugin` export default locals => { let { assets } = locals.webpackStats.compilation; diff --git a/src/utilities/content-utils.js b/src/utilities/content-utils.js new file mode 100644 index 000000000000..2bae423d4d22 --- /dev/null +++ b/src/utilities/content-utils.js @@ -0,0 +1,65 @@ +/** + * Walk the given tree of content + * + * @param {object} tree - Any node in the content tree + * @param {function} callback - Run on every descendant as well as the given `tree` + */ +export const WalkContent = (tree, callback) => { + callback(tree); + + if ( tree.children ) { + tree.children.forEach(child => { + WalkContent(child, callback); + }); + } +}; + +/** + * Deep flatten the given `tree`s child nodes + * + * @param {object} tree - Any node in the content tree + * @return {array} - A flattened list of leaf node descendants + */ +export const FlattenContent = tree => { + if ( tree.children ) { + return tree.children.reduce((flat, item) => { + return flat.concat( + Array.isArray(item.children) ? FlattenContent(item) : item + ); + }, []); + + } else return []; +}; + +/** + * Find an item within the given `tree` + * + * @param {object} tree - Any node in the content tree + * @param {function} test - A callback to find any leaf node in the given `tree` + * @return {object} - The first leaf node that passes the `test` + */ +export const FindInContent = (tree, test) => { + let list = FlattenContent(tree); + + return list.find(test); +}; + +/** + * Get top-level sections + * + * @param {object} tree - Any node in the content tree + * @return {array} - Immediate children of the given `tree` that are directories + */ +export const ExtractSections = tree => { + return tree.children.filter(item => item.type === 'directory'); +}; + +/** + * Get all markdown pages + * + * @param {object} tree - Any node in the content tree + * @return {array} - All markdown descendants of the given `tree` + */ +export const ExtractPages = tree => { + return FlattenContent(tree).filter(item => item.extension === '.md'); +}; diff --git a/webpack.common.js b/webpack.common.js index 10e06cc6bd2a..a1bf3263581e 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -109,7 +109,17 @@ module.exports = (env = {}) => ({ dir: 'src/content', path: 'src/_content.json', extensions: /\.md/, - enhance: treePluginEnhacer + enhance: treePluginEnhacer, + filter: item => item.name !== 'images', + sort: (a, b) => { + let group1 = (a.group || '').toLowerCase(); + let group2 = (b.group || '').toLowerCase(); + + if (group1 < group2) return -1; + if (group1 > group2) return 1; + if (a.sort && b.sort) return a.sort - b.sort; + else return 0; + } }) ], stats: {