├── .gitignore ├── README.md ├── babel.js ├── examples ├── a.js ├── app.js ├── b.js ├── index.html ├── index.js ├── static.js └── webpack.config.js ├── package.json ├── src ├── babel.js ├── extractEntries.js ├── extractResourceMap.js └── index.js └── tests └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # osx noise 2 | .DS_Store 3 | profile 4 | 5 | # xcode noise 6 | build/* 7 | *.mode1 8 | *.mode1v3 9 | *.mode2v3 10 | *.perspective 11 | *.perspectivev3 12 | *.pbxuser 13 | *.xcworkspace 14 | xcuserdata 15 | 16 | # svn & cvs 17 | .svn 18 | CVS 19 | example/*bundle.js 20 | node_modules 21 | lib 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-modules 2 | --- 3 | 4 | [work in progress] 5 | 6 | `npm install react-modules babel-template --save` 7 | 8 | code splitting as a component 9 | 10 | ```jsx 11 | import { Modules } from 'react-modules' 12 | 13 | { 15 | // or arrays, or objects, whatevs 16 | App => App ? 17 |
: 18 | loading... 19 | }
20 | ``` 21 | 22 | - isomorphic / SSR friendly 23 | - transpiles to [webpack-friendly split points](https://webpack.github.io/docs/code-splitting.html) with a plugin(`react-modules/babel`) 24 | - with helpers to preserve server-rendered html until module loads 25 | - leverage the structure of your app to efficiently split/load your javascript bundles 26 | 27 | 28 | api 29 | --- 30 | 31 | ## 32 | ```jsx 33 | { 34 | ([A, B { c:C }]) => {...} 35 | } 36 | ``` 37 | 38 | - `load={reqs}` - return the required modules with `require`. with the plugin, this will be converted to webpack code split points. 39 | - `onError={fn}` - catch errors 40 | - `include={bool}` - bypasses the code split 41 | - `defer={bool}` - loads the scripts only in the trasnpiled version 42 | - `chunkName={str}` - optional, acts as third argument to the backing `require.ensure()` call for named chunks 43 | - `entry={name}` - (experimental) include chunk into `name` entry. works in tandem with `extractEntries()` (TODO - accept multiple entries) 44 | 45 | 46 | ## html persistence helpers 47 | 48 | a set of helpers to preserve server/static-rendered html, until its 'parent' module loads. 49 | this is useful when the component is heavy, but you still want to show prerendered html while the chunk loads 50 | 51 | - `preserve(id, DOMelement) : DOMelement` 52 | - `preserved(id) : DOMelement` 53 | 54 | example - 55 | ```jsx 56 | { 57 | A => A ? preserve('myhtml',
): // on SSR, this will generate html 58 | preserved('myhtml') || // on browser, use the cached html, until the module loads up 59 | loading... // if neither available, show loading state 60 | }
61 | ``` 62 | 63 | Use sparingly! This will probably break react's checksum algo, but that's the tradeoff you'll need for this behavior. 64 | 65 | NB: to prime the cache, import and call `hydrate()` right before you call `ReactDOM.render()`. 66 | 67 | ## plugin 68 | 69 | - `react-modules/babel` - wraps `Modules` components' `load` props with `require.ensure` boilerplate, generating code splits 70 | 71 | ## extractEntries 72 | 73 | - `extractEntries(filepath)` (experimental) - statically analyze module and generate webpack entries 74 | 75 | ## extractResourceMap 76 | - `extractResourceMap(filepath)` (experimental) - statically analyze an app and generate urlpattern -> entries map. works in tandem with react-router@4. 77 | 78 | todo 79 | --- 80 | 81 | - docs 82 | - tests 83 | - custom `` component that accepts entry/load 84 | - `express` helper/middleware to serve bundles 85 | - hmr compat 86 | - arbit file types / webpack loader compat 87 | - browserify compat 88 | - react-native 89 | -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/babel.js') 2 | -------------------------------------------------------------------------------- /examples/a.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class A extends React.Component { 4 | render() { 5 | return
we here in A
6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Modules } from '../src' 3 | import { Match, Miss, Link } from 'react-router' 4 | 5 | export class App extends React.Component { 6 | 7 | render() { 8 | return
9 | 10 |
    11 |
  • home
  • 12 |
  • to a
  • 13 |
  • to b
  • 14 |
  • 404
  • 15 |
16 | 17 | 18 |
we home
}/> 19 | 20 | 21 | { 22 | A => A ? : loading A... 23 | }} /> 24 | 25 | 26 | { 27 | B => B ? : loading B... 28 | }}/> 29 | 30 | no match}/> 31 | 32 |
33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/b.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class B extends React.Component { 4 | render() { 5 | return
we here in B
6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | react-modules 3 |
4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { BrowserRouter } from 'react-router' 4 | import { App } from './app' 5 | 6 | render( 7 | 8 | , document.getElementById('app')) 9 | -------------------------------------------------------------------------------- /examples/static.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToString } from 'react-dom/server' 3 | import { App } from './app' 4 | 5 | import { ServerRouter, createServerRenderContext } from 'react-router' 6 | const context = createServerRenderContext() 7 | 8 | let markup = renderToString( 9 | 12 | 13 | 14 | ) 15 | 16 | console.log(markup) 17 | 18 | // // the result will tell you if it redirected, if so, we ignore 19 | // // the markup and send a proper redirect. 20 | // if (result.redirect) { 21 | // res.writeHead(301, { 22 | // Location: result.redirect.pathname 23 | // }) 24 | // res.end() 25 | // } else { 26 | 27 | // // the result will tell you if there were any misses, if so 28 | // // we can send a 404 and then do a second render pass with 29 | // // the context to clue the components into rendering 30 | // // this time (on the client they know from componentDidMount) 31 | // if (result.missed) { 32 | // res.writeHead(404) 33 | // markup = renderToString( 34 | // 38 | // 39 | // 40 | // ) 41 | // } 42 | // res.write(markup) 43 | // res.end() 44 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') // eslint-disable-line 2 | var extractEntries = require('../lib/extractEntries').default // eslint-disable-line 3 | 4 | module.exports = { 5 | entry: extractEntries(path.join(__dirname, './index.js')), 6 | output: { 7 | path: __dirname, 8 | filename: '[name].bundle.js', 9 | chunkFilename: '[name].chunk.js' 10 | }, 11 | module: { 12 | loaders: [ { 13 | test: /\.js$/, 14 | loader: 'babel-loader', 15 | query: { 16 | presets: [ 'es2015', 'stage-0', 'react' ], 17 | plugins: [ path.resolve('./src/babel') ] 18 | } 19 | } ] 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-modules", 3 | "version": "1.0.8", 4 | "description": "code splitting as a component", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config examples/webpack.config.js --content-base examples/ --history-api-fallback --compress --inline", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "babel src -d lib --presets es2015,stage-0,react", 10 | "prepublish": "npm run build" 11 | }, 12 | "files": [ 13 | "lib", 14 | "babel.js" 15 | ], 16 | "author": "Sunil Pai ", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "babel-cli": "^6.14.0", 20 | "babel-eslint": "^6.1.2", 21 | "babel-loader": "^6.2.5", 22 | "babel-preset-es2015": "^6.14.0", 23 | "babel-preset-react": "^6.11.1", 24 | "babel-preset-stage-0": "^6.5.0", 25 | "babel-template": "^6.15.0", 26 | "eslint": "^3.5.0", 27 | "eslint-config-rackt": "^1.1.1", 28 | "eslint-plugin-react": "^6.3.0", 29 | "react": "^15.3.2", 30 | "react-dom": "^15.3.2", 31 | "react-router": "^4.0.0-alpha.3", 32 | "webpack": "^1.13.2", 33 | "webpack-dev-server": "^1.16.1" 34 | }, 35 | "peerDependencies": { 36 | "react": "*" 37 | }, 38 | "eslintConfig": { 39 | "extends": [ 40 | "rackt" 41 | ], 42 | "plugins": [ 43 | "react" 44 | ], 45 | "rules": { 46 | "react/jsx-uses-vars": "error", 47 | "react/jsx-uses-react": "error" 48 | } 49 | }, 50 | "dependencies": { 51 | "babel-template": "*", 52 | "babel-traverse": "^6.15.0", 53 | "babylon": "^6.10.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/babel.js: -------------------------------------------------------------------------------- 1 | let template = require('babel-template') 2 | 3 | let boilerplate = `callback => require.ensure([], require => { 4 | let success = false, ret 5 | try{ 6 | ret = SOURCE 7 | success = true 8 | } 9 | catch(err) { 10 | callback(err) 11 | } 12 | if(success){ 13 | callback(null, ret) 14 | } 15 | }` 16 | 17 | let wrapper = template(boilerplate + ')') 18 | 19 | let wrapperWithName = template(boilerplate + ', NAME)') 20 | 21 | let TRUE = template('true') 22 | 23 | function replace(attr, name) { 24 | let val = (name ? wrapperWithName : wrapper)({ SOURCE: attr.value.expression, NAME: name }) 25 | attr.value.expression = val.expression 26 | } 27 | 28 | module.exports = function ({ types: t }) { 29 | return { 30 | visitor: { 31 | JSXElement(path) { 32 | if(path.node.openingElement.name.name === 'Modules') { 33 | let chunkName = path.node.openingElement.attributes.filter(attr => 34 | attr.name.name === 'chunkName')[0] 35 | chunkName = chunkName ? chunkName.value : undefined 36 | 37 | let included = path.node.openingElement.attributes.filter(attr => 38 | attr.name.name === 'include').length > 0 39 | 40 | if(!included) { 41 | path.node.openingElement.attributes.forEach(attr => 42 | attr.name.name === 'load' && replace(attr, chunkName) ) 43 | 44 | path.node.openingElement.attributes.push( 45 | t.jSXAttribute(t.jSXIdentifier('transpiled'), 46 | t.jSXExpressionContainer(TRUE().expression))) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/extractEntries.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | 4 | import * as babylon from 'babylon' 5 | import traverse from 'babel-traverse' 6 | 7 | export default function extract(file) { 8 | // iterate through files / dependency tree 9 | // parse with babel / detect { 15 | let ext = path.extname(f) 16 | if(ext === '.js') { 17 | let src = fs.readFileSync(path.join(dir, f), 'utf8') 18 | let ast = babylon.parse(src, { 19 | sourceType: 'module', 20 | plugins: [ 'jsx', 'flow', 'doExpressions', 'objectRestSpread', 'decorators', 'classProperties', 21 | 'exportExtensions', 'asyncGenerators', 'functionBind', 'functionSent' ] 22 | }) 23 | traverse(ast, { 24 | enter(x) { 25 | if(x.type === 'JSXElement' && x.node.openingElement.name.name === 'Modules') { 26 | let attrs = x.node.openingElement.attributes 27 | let hasEntry = attrs.filter(attr => attr.name.name === 'entry') 28 | if(hasEntry.length > 0) { 29 | hasEntry = hasEntry[0].value.value 30 | } 31 | else { 32 | hasEntry = undefined 33 | } 34 | if(hasEntry) { 35 | let hasLoad = attrs.filter(attr => attr.name.name === 'load') 36 | if(hasLoad.length > 0) { 37 | hasLoad = hasLoad[0].value.expression 38 | } 39 | else { 40 | hasLoad = undefined 41 | } 42 | 43 | // parse hasload for all require calls 44 | if(hasLoad) { 45 | let reqs = src.substring(hasLoad.start, hasLoad.end) 46 | let regex = /require\(['"](.*?)['"]\)/g 47 | let m, matches = [] 48 | while ((m = regex.exec(reqs)) !== null) { 49 | matches.push(path.join(dir, m[1])) 50 | } 51 | if(matches.length > 0) { 52 | ret[hasEntry] = ret[hasEntry] || [] 53 | ret[hasEntry] = ret[hasEntry].concat(matches) 54 | } 55 | 56 | } 57 | } 58 | } 59 | } 60 | }) 61 | } 62 | }) 63 | Object.keys(ret).forEach(key => { 64 | ret[key].push(file) 65 | }) 66 | return ret 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/extractResourceMap.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | 4 | import * as babylon from 'babylon' 5 | import traverse from 'babel-traverse' 6 | 7 | 8 | export default function extract(file) { 9 | // iterate through files / dependency tree 10 | // parse with babel / detect { 16 | let ext = path.extname(f) 17 | if(ext === '.js') { 18 | let src = fs.readFileSync(path.join(dir, f), 'utf8') 19 | let ast = babylon.parse(src, { 20 | sourceType: 'module', 21 | plugins: [ 'jsx', 'flow', 'doExpressions', 'objectRestSpread', 'decorators', 'classProperties', 22 | 'exportExtensions', 'asyncGenerators', 'functionBind', 'functionSent' ] 23 | }) 24 | traverse(ast, { 25 | enter(x) { 26 | if(x.type === 'JSXElement' && x.node.openingElement.name.name === 'Match') { 27 | 28 | let attrs = x.node.openingElement.attributes 29 | let hasPattern = attrs.filter(attr => attr.name.name === 'pattern') 30 | if(hasPattern.length > 0) { 31 | hasPattern = hasPattern[0].value.value 32 | } 33 | else { 34 | hasPattern = undefined 35 | } 36 | if(hasPattern) { 37 | // pull out entries 38 | let entries = src.substring(x.node.start, x.node.end) 39 | let regex = /entry\=['"](.*?)['"]/g 40 | let m, matches = [] 41 | while ((m = regex.exec(entries)) !== null) { 42 | matches.push(m[1]) 43 | } 44 | if(matches.length > 0) { 45 | ret[hasPattern] = ret[hasPattern] || [] 46 | ret[hasPattern] = ret[hasPattern].concat(matches) 47 | } 48 | } 49 | } 50 | } 51 | }) 52 | } 53 | }) 54 | return ret 55 | } 56 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | let isBrowser = typeof document !== 'undefined' 3 | 4 | export class Modules extends React.Component { 5 | // static defaultProps = { 6 | // onError() {} 7 | // } 8 | constructor(props) { 9 | super(props) 10 | if(!this.props.transpiled) { 11 | if(this.props.defer) { 12 | // todo - prevent evaluation? 13 | return 14 | } 15 | this.state = { 16 | loaded: this.props.load 17 | } 18 | return 19 | } 20 | 21 | // possible async load 22 | this.state = { 23 | loaded: undefined 24 | } 25 | let sync = true 26 | 27 | this.props.load((err, loaded) => { 28 | if(err) { 29 | if(!this.props.onError) { 30 | throw err 31 | } 32 | this.props.onError(err) 33 | } 34 | if(sync) { 35 | this.state.loaded = loaded 36 | } 37 | else { 38 | this.setState({ loaded }) 39 | } 40 | }) 41 | sync = false 42 | } 43 | unstable_handleError(err) { 44 | if(!this.props.onError) { 45 | throw err 46 | } 47 | this.props.onError(err) 48 | } 49 | 50 | componentWillReceiveProps() { 51 | // hot loading and stuff 52 | } 53 | 54 | render() { 55 | return this.props.children(this.state.loaded) 56 | } 57 | } 58 | 59 | 60 | let cache = {} 61 | export function hydrate() { 62 | // get all data-preserve 63 | // cache innerhtml 64 | [ ...document.querySelectorAll('[data-preserve]') ].forEach(el => { 65 | let id = el.getAttribute('data-preserve') 66 | if(cache[id]) { 67 | console.warn(`overwriting previous key ${id}!`) // eslint-disable-line no-console 68 | } 69 | cache[id] = el.innerHTML 70 | }) 71 | } 72 | 73 | export function flush() { 74 | cache = {} 75 | } 76 | 77 | export function preserve(id, element) { 78 | if(!(typeof element.type === 'string')) { 79 | throw new Error('cannot preserve non-DOM element') 80 | } 81 | return React.cloneElement(element, { 'data-preserve': id }) 82 | } 83 | 84 | 85 | export function preserved(id, Tag = 'div', props = {}) { 86 | return cache[id] ? : undefined 87 | } 88 | 89 | 90 | // isBrowser && hydrate() // whate harm could this do 91 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | // how do I even test this --------------------------------------------------------------------------------