@untool/react
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -6,2 +6,21 @@ # Change Log | ||
# [1.2.0](https://github.com/untool/untool/compare/v1.1.0...v1.2.0) (2019-02-27) | ||
### Bug Fixes | ||
* **react:** only allow arrow function with single import() statement ([2bbbdcc](https://github.com/untool/untool/commit/2bbbdcc)) | ||
### Features | ||
* **react:** add arg validation to mixin methods ([c03e897](https://github.com/untool/untool/commit/c03e897)) | ||
* **react:** add arg/prop validation to public API ([9150954](https://github.com/untool/untool/commit/9150954)) | ||
* **react:** allow function as namespace resolver ([11d20a2](https://github.com/untool/untool/commit/11d20a2)) | ||
* **react:** allow import load function in importComponent ([dbeea04](https://github.com/untool/untool/commit/dbeea04)) | ||
# [1.1.0](https://github.com/untool/untool/compare/v1.0.0...v1.1.0) (2019-02-14) | ||
@@ -8,0 +27,0 @@ |
@@ -23,5 +23,21 @@ 'use strict'; | ||
const argument = call.get('arguments')[0]; | ||
t.assertStringLiteral(argument); | ||
const argument = call.get('arguments.0'); | ||
if (!argument) { | ||
throw new Error( | ||
'"importComponent" must be called with at least one parameter!' | ||
); | ||
} | ||
let importedComponent; | ||
if (t.isStringLiteral(argument)) { | ||
importedComponent = argument.node.value; | ||
} else { | ||
t.assertArrowFunctionExpression(argument); | ||
t.assertCallExpression(argument.get('body')); | ||
t.assertImport(argument.get('body.callee')); | ||
importedComponent = argument.get('body.arguments.0').node.value; | ||
} | ||
argument.replaceWith( | ||
@@ -33,3 +49,5 @@ t.objectExpression([ | ||
[], | ||
t.callExpression(t.identifier('import'), [argument.node]) | ||
t.callExpression(t.identifier('import'), [ | ||
t.stringLiteral(importedComponent), | ||
]) | ||
) | ||
@@ -44,3 +62,3 @@ ), | ||
), | ||
[argument.node] | ||
[t.stringLiteral(importedComponent)] | ||
) | ||
@@ -47,0 +65,0 @@ ), |
'use strict'; | ||
/* global __webpack_modules__, __webpack_require__ */ | ||
const { createElement, Component } = require('react'); | ||
const { isValidElement, createElement, Component } = require('react'); | ||
const { default: withRouter } = require('react-router-dom/es/withRouter'); | ||
const isPlainObject = require('is-plain-object'); | ||
const PropTypes = require('prop-types'); | ||
const { initialize } = require('@untool/core'); | ||
const { | ||
initialize, | ||
internal: { invariant }, | ||
} = require('@untool/core'); | ||
exports.render = (element, options) => (...args) => { | ||
invariant( | ||
isValidElement(element), | ||
'render(): Received invalid React element' | ||
); | ||
invariant( | ||
options === undefined || isPlainObject(options), | ||
'render(): Received invalid options' | ||
); | ||
const { render } = initialize({}, element, options); | ||
if (!render) { | ||
throw new Error("Can't use @untool/react mixin"); | ||
} | ||
invariant(render, "Can't use @untool/react mixin"); | ||
return render(...args); | ||
}; | ||
exports.Miss = withRouter(function Miss({ staticContext }) { | ||
const Miss = ({ staticContext }) => { | ||
if (staticContext) { | ||
@@ -22,5 +33,9 @@ staticContext.miss = true; | ||
return null; | ||
}); | ||
}; | ||
Miss.propTypes = { | ||
staticContext: PropTypes.object, | ||
}; | ||
exports.Miss = withRouter(Miss); | ||
exports.Status = withRouter(function Status({ staticContext, code }) { | ||
const Status = ({ staticContext, code }) => { | ||
if (staticContext) { | ||
@@ -30,5 +45,10 @@ staticContext.status = code; | ||
return null; | ||
}); | ||
}; | ||
Status.propTypes = { | ||
staticContext: PropTypes.object, | ||
code: PropTypes.number.isRequired, | ||
}; | ||
exports.Status = withRouter(Status); | ||
exports.Header = withRouter(function Header({ staticContext, name, value }) { | ||
const Header = ({ staticContext, name = '', value = '' }) => { | ||
if (staticContext) { | ||
@@ -38,45 +58,57 @@ staticContext.headers = { ...staticContext.headers, [name]: value }; | ||
return null; | ||
}); | ||
}; | ||
Header.propTypes = { | ||
staticContext: PropTypes.object, | ||
name: PropTypes.string.isRequired, | ||
value: PropTypes.string.isRequired, | ||
}; | ||
exports.Header = withRouter(Header); | ||
exports.importComponent = ({ load, moduleId }, name = 'default') => { | ||
const Importer = withRouter( | ||
class Importer extends Component { | ||
constructor({ staticContext }) { | ||
super(); | ||
if (staticContext) { | ||
staticContext.modules.push(moduleId); | ||
} | ||
if (staticContext || __webpack_modules__[moduleId]) { | ||
this.state = { Component: __webpack_require__(moduleId)[name] }; | ||
} else { | ||
this.state = { loading: true }; | ||
} | ||
const resolve = typeof name === 'function' ? name : (module) => module[name]; | ||
class Importer extends Component { | ||
constructor({ staticContext }) { | ||
super(); | ||
if (staticContext) { | ||
staticContext.modules.push(moduleId); | ||
} | ||
componentDidMount() { | ||
const { loader } = this.props; | ||
const { loading } = this.state; | ||
if (loading) { | ||
const state = { Component: null, error: null, loading: false }; | ||
Promise.resolve() | ||
.then(() => (loader ? loader(load) : load())) | ||
.then( | ||
({ [name]: Component }) => this.setState({ ...state, Component }), | ||
(error) => this.setState({ ...state, error }) | ||
); | ||
} | ||
if (staticContext || __webpack_modules__[moduleId]) { | ||
this.state = { Component: resolve(__webpack_require__(moduleId)) }; | ||
} else { | ||
this.state = { loading: true }; | ||
} | ||
render() { | ||
const { | ||
render = ({ Component, error, loading, ...props }) => { | ||
return !(error || loading) ? createElement(Component, props) : null; | ||
}, | ||
ownProps, | ||
} = this.props; | ||
return render({ ...ownProps, ...this.state }); | ||
} | ||
componentDidMount() { | ||
const { loader } = this.props; | ||
const { loading } = this.state; | ||
if (loading) { | ||
const state = { Component: null, error: null, loading: false }; | ||
Promise.resolve() | ||
.then(() => (loader ? loader(load) : load())) | ||
.then( | ||
(module) => this.setState({ ...state, Component: resolve(module) }), | ||
(error) => this.setState({ ...state, error }) | ||
); | ||
} | ||
} | ||
); | ||
render() { | ||
const { | ||
render = ({ Component, error, loading, ...props }) => { | ||
return !(error || loading) ? createElement(Component, props) : null; | ||
}, | ||
ownProps, | ||
} = this.props; | ||
return render({ ...ownProps, ...this.state }); | ||
} | ||
} | ||
Importer.propTypes = { | ||
staticContext: PropTypes.object, | ||
ownProps: PropTypes.object.isRequired, | ||
loader: PropTypes.func, | ||
render: PropTypes.func, | ||
}; | ||
const ImporterWithRouter = withRouter(Importer); | ||
return function Import({ loader, render, ...ownProps }) { | ||
return createElement(Importer, { loader, render, ownProps }); | ||
return createElement(ImporterWithRouter, { loader, render, ownProps }); | ||
}; | ||
}; |
@@ -5,6 +5,8 @@ 'use strict'; | ||
const { createElement } = require('react'); | ||
const { createElement, isValidElement } = require('react'); | ||
const { unmountComponentAtNode, hydrate, render } = require('react-dom'); | ||
const { default: BrowserRouter } = require('react-router-dom/es/BrowserRouter'); | ||
const isPlainObject = require('is-plain-object'); | ||
const { | ||
@@ -15,3 +17,6 @@ override, | ||
const { Mixin } = require('@untool/core'); | ||
const { | ||
Mixin, | ||
internal: { validate, invariant }, | ||
} = require('@untool/core'); | ||
@@ -50,8 +55,44 @@ class ReactMixin extends Mixin { | ||
ReactMixin.strategies = { | ||
bootstrap: parallel, | ||
enhanceElement: compose, | ||
fetchData: pipe, | ||
render: override, | ||
bootstrap: validate(parallel, ({ length }) => { | ||
invariant(length === 0, 'bootstrap(): Received unexpected argument(s)'); | ||
}), | ||
enhanceElement: validate( | ||
compose, | ||
([element]) => { | ||
invariant( | ||
isValidElement(element), | ||
'enhanceElement(): Received invalid React element' | ||
); | ||
}, | ||
(result) => { | ||
invariant( | ||
isValidElement(result), | ||
'enhanceElement(): Returned invalid React element' | ||
); | ||
} | ||
), | ||
fetchData: validate( | ||
pipe, | ||
([data, element]) => { | ||
invariant( | ||
isPlainObject(data), | ||
'fetchData(): Received invalid data object' | ||
); | ||
invariant( | ||
isValidElement(element), | ||
'fetchData(): Received invalid React element' | ||
); | ||
}, | ||
(result) => { | ||
invariant( | ||
isPlainObject(result), | ||
'fetchData(): Returned invalid data object' | ||
); | ||
} | ||
), | ||
render: validate(override, ({ length }) => { | ||
invariant(length === 0, 'render(): Received unexpected argument(s)'); | ||
}), | ||
}; | ||
module.exports = ReactMixin; |
@@ -5,3 +5,3 @@ 'use strict'; | ||
const { createElement } = require('react'); | ||
const { createElement, isValidElement } = require('react'); | ||
const { renderToString } = require('react-dom/server'); | ||
@@ -13,2 +13,4 @@ const { default: StaticRouter } = require('react-router-dom/es/StaticRouter'); | ||
const isPlainObject = require('is-plain-object'); | ||
const { | ||
@@ -19,3 +21,6 @@ override, | ||
const { Mixin } = require('@untool/core'); | ||
const { | ||
Mixin, | ||
internal: { validate, invariant }, | ||
} = require('@untool/core'); | ||
@@ -79,9 +84,77 @@ const getAssets = require('../lib/assets'); | ||
ReactMixin.strategies = { | ||
bootstrap: parallel, | ||
enhanceElement: compose, | ||
fetchData: pipe, | ||
getTemplateData: pipe, | ||
render: override, | ||
bootstrap: validate(parallel, ([req, res]) => { | ||
invariant( | ||
req && req.app && req.url, | ||
'bootstrap(): Received invalid HTTP request object' | ||
); | ||
invariant( | ||
res && res.app && res.locals, | ||
'bootstrap(): Received invalid HTTP response object' | ||
); | ||
}), | ||
enhanceElement: validate( | ||
compose, | ||
([element]) => { | ||
invariant( | ||
isValidElement(element), | ||
'enhanceElement(): Received invalid React element' | ||
); | ||
}, | ||
(result) => { | ||
invariant( | ||
isValidElement(result), | ||
'enhanceElement(): Returned invalid React element' | ||
); | ||
} | ||
), | ||
fetchData: validate( | ||
pipe, | ||
([data, element]) => { | ||
invariant( | ||
isPlainObject(data), | ||
'fetchData(): Received invalid data object' | ||
); | ||
invariant( | ||
isValidElement(element), | ||
'fetchData(): Received invalid React element' | ||
); | ||
}, | ||
(result) => { | ||
invariant( | ||
isPlainObject(result), | ||
'fetchData(): Returned invalid data object' | ||
); | ||
} | ||
), | ||
getTemplateData: validate( | ||
pipe, | ||
([data]) => { | ||
invariant( | ||
isPlainObject(data), | ||
'getTemplateData(): Received invalid data object' | ||
); | ||
}, | ||
(result) => { | ||
invariant( | ||
isPlainObject(result), | ||
'getTemplateData(): Returned invalid data object' | ||
); | ||
} | ||
), | ||
render: validate(override, ([req, res, next]) => { | ||
invariant( | ||
req && req.app && req.url, | ||
'render(): Received invalid HTTP request object' | ||
); | ||
invariant( | ||
res && res.app && res.locals, | ||
'render(): Received invalid HTTP response object' | ||
); | ||
invariant( | ||
typeof next === 'function', | ||
'render(): Received invalid next() function' | ||
); | ||
}), | ||
}; | ||
module.exports = ReactMixin; |
{ | ||
"name": "@untool/react", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "untool react mixin", | ||
@@ -23,6 +23,8 @@ "browser": "lib/runtime.js", | ||
"@babel/preset-react": "^7.0.0", | ||
"@untool/core": "^1.1.0", | ||
"@untool/core": "^1.2.0", | ||
"babel-plugin-transform-react-remove-prop-types": "^0.4.19", | ||
"clone": "^2.1.2", | ||
"is-plain-object": "^2.0.4", | ||
"mixinable": "^4.0.0", | ||
"prop-types": "^15.7.2", | ||
"serialize-javascript": "^1.4.0" | ||
@@ -39,3 +41,3 @@ }, | ||
}, | ||
"gitHead": "e92a4e17440f172b7211c57d7bbf9badd34ec8b3" | ||
"gitHead": "0576dcfa7e743599b9f020024e1f479cdd7044b9" | ||
} |
@@ -56,3 +56,3 @@ # `@untool/react` | ||
With this component, you can declaratively set arbitrary HTTP headers from your React application. | ||
With this component, you can declaratively set arbitrary HTTP headers from your React application. On the client side, it is effectively a no-op. | ||
@@ -65,3 +65,3 @@ ```javascript | ||
### `importComponent(module, [exportName])` | ||
### `importComponent(module|moduleLoader, [exportName|exportResolver])` | ||
@@ -73,3 +73,3 @@ Using the `importComponent` helper, you can asynchronously require components into your application to help you reduce asset sizes. It works similarly to [`react-loadable`](https://github.com/jamiebuilds/react-loadable), but is deeply integrated with `untool`. | ||
const Home = importComponent('./home'); | ||
const Home = importComponent('./home', 'Home'); | ||
@@ -79,2 +79,14 @@ export default () => <Home />; | ||
Additionally, `importComponent` supports an alternative syntax that helps with editor and type checker integration since it does not rely on plain strings. The snippet below is functionally equivalent to the one above: | ||
```javascript | ||
import { importComponent } from '@untool/react'; | ||
const Home = importComponent(() => import('./home'), ({ Home }) => Home); | ||
export default () => <Home />; | ||
``` | ||
If you do no specify an `exportName` or `exportResolver`, `importComponent` will fall back to the imported modules `default` export. | ||
`importComponent` itself returns a React component supporting some props that enable you to control module loading and (placeholder) rendering. | ||
@@ -81,0 +93,0 @@ |
39314
483
195
13
+ Addedis-plain-object@^2.0.4
+ Addedprop-types@^15.7.2
+ Addedis-plain-object@2.0.4(transitive)
+ Addedisobject@3.0.1(transitive)
Updated@untool/core@^1.2.0