├── .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
--------------------------------------------------------------------------------