diff --git a/.changeset/brown-fishes-exercise.md b/.changeset/brown-fishes-exercise.md new file mode 100644 index 00000000..7215082b --- /dev/null +++ b/.changeset/brown-fishes-exercise.md @@ -0,0 +1,5 @@ +--- +'preact-render-to-string': major +--- + +Support Preact v11, this breaks compat with Preact 10 diff --git a/jsx.d.ts b/jsx.d.ts index ee2ba580..6645ee5a 100644 --- a/jsx.d.ts +++ b/jsx.d.ts @@ -1,13 +1,14 @@ import { VNode } from 'preact'; interface Options { - jsx?: boolean; - xml?: boolean; - functions?: boolean - functionNames?: boolean, - skipFalseAttributes?: boolean - pretty?: boolean | string; + jsx?: boolean; + xml?: boolean; + functions?: boolean; + functionNames?: boolean; + skipFalseAttributes?: boolean; + pretty?: boolean | string; + errorBoundaries?: boolean; } -export function render(vnode: VNode, context?: any, options?: Options):string; +export function render(vnode: VNode, context?: any, options?: Options): string; export default render; diff --git a/package-lock.json b/package-lock.json index 96391807..5f99f9a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,14 +26,14 @@ "lint-staged": "^10.5.3", "microbundle": "^0.13.0", "mocha": "^8.2.1", - "preact": "^10.5.7", + "preact": "^11.0.0-experimental.0", "prettier": "^2.2.1", "sinon": "^9.2.2", "sinon-chai": "^3.5.0", "typescript": "^4.1.3" }, "peerDependencies": { - "preact": ">=10" + "preact": ">=11" } }, "node_modules/@babel/code-frame": { @@ -10664,10 +10664,14 @@ } }, "node_modules/preact": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.7.tgz", - "integrity": "sha512-4oEpz75t/0UNcwmcsjk+BIcDdk68oao+7kxcpc1hQPNs2Oo3ZL9xFz8UBf350mxk/VEdD41L5b4l2dE3Ug3RYg==", - "dev": true + "version": "11.0.0-experimental.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-11.0.0-experimental.0.tgz", + "integrity": "sha512-TXVw49O11z34ouJOe9+wzN9/ReoXoNyO1Jth48SiJDuqLKrdAiuXkBlmtTWeDgiaJr1SSPDMBdLufMDjDkb5bg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } }, "node_modules/preferred-pm": { "version": "3.0.3", @@ -22209,9 +22213,9 @@ "dev": true }, "preact": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.7.tgz", - "integrity": "sha512-4oEpz75t/0UNcwmcsjk+BIcDdk68oao+7kxcpc1hQPNs2Oo3ZL9xFz8UBf350mxk/VEdD41L5b4l2dE3Ug3RYg==", + "version": "11.0.0-experimental.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-11.0.0-experimental.0.tgz", + "integrity": "sha512-TXVw49O11z34ouJOe9+wzN9/ReoXoNyO1Jth48SiJDuqLKrdAiuXkBlmtTWeDgiaJr1SSPDMBdLufMDjDkb5bg==", "dev": true }, "preferred-pm": { diff --git a/package.json b/package.json index 9b1df539..bbcfe7e2 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "bugs": "https://github.com/developit/preact-render-to-string/issues", "homepage": "https://github.com/developit/preact-render-to-string", "peerDependencies": { - "preact": ">=10" + "preact": ">=11" }, "devDependencies": { "@babel/plugin-transform-react-jsx": "^7.12.12", @@ -114,7 +114,7 @@ "lint-staged": "^10.5.3", "microbundle": "^0.13.0", "mocha": "^8.2.1", - "preact": "^10.5.7", + "preact": "^11.0.0-experimental.0", "prettier": "^2.2.1", "sinon": "^9.2.2", "sinon-chai": "^3.5.0", diff --git a/src/index.d.ts b/src/index.d.ts index 221d349a..30c63b30 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -4,6 +4,8 @@ interface Options { shallow?: boolean; xml?: boolean; pretty?: boolean | string; + /** Enable or disable error boundaries (default: false) */ + errorBoundaries?: boolean; } export function render(vnode: VNode, context?: any, options?: Options): string; diff --git a/src/index.js b/src/index.js index 7714f36f..8fa99374 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,8 @@ import { isLargeString, styleObjToCss, assign, - getChildren + getChildren, + createInternalFromVnode } from './util'; import { options, Fragment } from 'preact'; @@ -15,8 +16,7 @@ const SHALLOW = { shallow: true }; // components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names. const UNNAMED = []; -const VOID_ELEMENTS = - /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; +const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; const UNSAFE_NAME = /[\s\n\\/='"\0<>]/; @@ -32,6 +32,7 @@ const noop = () => {}; * @param {Boolean} [options.xml=false] If `true`, uses self-closing tags for elements without children. * @param {Boolean} [options.pretty=false] If `true`, adds whitespace for readability * @param {RegExp|undefined} [options.voidElements] RegeEx that matches elements that are considered void (self-closing) + * @param {boolean|undefined} [options.errorBoundaries=false] Enables support for error boundaries */ renderToString.render = renderToString; @@ -128,14 +129,15 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { setState: noop, forceUpdate: noop, // hooks - __h: [] + data: {} }); // options._diff if (options.__b) options.__b(vnode); + const internal = createInternalFromVnode(vnode, context); // options._render - if (options.__r) options.__r(vnode); + if (options.__r) options.__r(internal); if ( !nodeName.prototype || @@ -204,6 +206,53 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { } if (options.diffed) options.diffed(vnode); + + if ( + opts.errorBoundaries && + (c.componentDidCatch || nodeName.getDerivedStateFromError) + ) { + try { + return _renderToString( + rendered, + context, + opts, + opts.shallowHighOrder !== false, + isSvgMode, + selectValue + ); + } catch (err) { + if (nodeName.getDerivedStateFromError) { + let nextState = nodeName.getDerivedStateFromError(err); + c.state = c._nextState = c.__s = Object.assign( + {}, + c.state, + nextState + ); + } + + if (c.componentDidCatch) c.componentDidCatch(err, {}); + + let nextState = + c._nextState !== c.state + ? c._nextState + : c.__s !== c.state + ? c.__s + : c.state; + + // Check if there is a potential state update. Note that + // we'll ignore any state updates inside cWU for now + // because that's an infinite loop anyway in the browser + if (c.state !== nextState && c.componentWillUpdate) { + c.componentWillUpdate(); + } + + // Flush potential state updates + c.state = nextState; + + rendered = c.render(c.props, c.state, c.context); + } + } + return _renderToString( rendered, context, diff --git a/src/jsx.d.ts b/src/jsx.d.ts index 98fad555..8dcafef3 100644 --- a/src/jsx.d.ts +++ b/src/jsx.d.ts @@ -3,6 +3,8 @@ import { VNode } from 'preact'; interface Options { jsx?: boolean; xml?: boolean; + /** Enable or disable error boundaries (default: false) */ + errorBoundaries?: boolean; functions?: boolean; functionNames?: boolean; skipFalseAttributes?: boolean; diff --git a/src/util.js b/src/util.js index 5ade429e..48840878 100644 --- a/src/util.js +++ b/src/util.js @@ -76,3 +76,12 @@ export function getChildren(accumulator, children) { } return accumulator; } + +export function createInternalFromVnode(vnode, context) { + return { + type: vnode.type, + props: vnode.props, + data: {}, + c: context + }; +} diff --git a/test/compat.test.js b/test/compat.test.js index e6fc6b96..e51fa096 100644 --- a/test/compat.test.js +++ b/test/compat.test.js @@ -1,5 +1,6 @@ import { render } from '../src'; -import { createElement } from 'preact/compat'; +import { h } from 'preact'; +import { createElement, Component } from 'preact/compat'; import { expect } from 'chai'; describe('compat', () => { @@ -9,4 +10,50 @@ describe('compat', () => { expect(rendered).to.equal(expected); }); + + it('should apply defaultProps (func)', () => { + const Test = (props) =>
; + Test.defaultProps = { + foo: 'default foo', + bar: 'default bar' + }; + + expect(render({this.state.error.message}
+ ) : ( +{this.state.error.message}
+ ) : ( +fail
'); + expect(args).to.deep.equal({ error: 'fail', info: {} }); + }); + + it("should not invoke parent's componentDidCatch", () => { + class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + componentDidCatch(error) { + this.setState({ error }); + } + + render() { + return this.state.error ? ( +{this.state.error.message}
+ ) : ( +it doesn't work
; + } + } + + let args = null; + class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + componentDidCatch(error, info) { + args = { error: error.message, info }; + this.setState({ error }); + } + + render() { + return this.state.error ? ( +{this.state.error.message}
+ ) : ( +fail
'); + expect(args).to.deep.equal({ error: 'fail', info: {} }); + + expect(throwerCatchCalled).to.equal( + false, + "Thrower's componentDidCatch should not be called" + ); + }); + + it('should invoke componentWillUpdate on state render', () => { + let calledWillUpdate = false; + + class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + componentWillUpdate() { + calledWillUpdate = true; + } + componentDidCatch(error, info) { + this.setState({ error }); + } + + render() { + return this.state.error ? ( +{this.state.error.message}
+ ) : ( +fail
'); + expect(calledWillUpdate).to.equal( + true, + 'Did not call componentWillUpdate' + ); + }); + }); + + describe('getDerivedStateFromError', () => { + it('should disable gDSFE by default', () => { + class ErrorBoundary extends Component { + static getDerivedStateFromError(error) { + return { error }; + } + + render() { + return this.state.error ? ( +{this.state.error.message}
+ ) : ( +it works
:it works
'); + expect(calls).to.deep.equal([ + ['gDSFE', 'fail'], + ['cDC', 'fail'] + ]); + expect(cDCState).to.deep.equal({ + foo: 1, + error: null + }); + }); + + it('should work without componentDidCatch', () => { + class ErrorBoundary extends Component { + static getDerivedStateFromError(error) { + return { error: error.message }; + } + + render() { + return this.state.error ?{this.state.error}
:fail
'); + }); + }); + }); });Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: