Skip to content

Commit 7e84169

Browse files
authored
Merge branch 'master' into experiment-remove-conditional-error-bubbling
2 parents 90a1d84 + dd1e281 commit 7e84169

19 files changed

Lines changed: 9401 additions & 9047 deletions

babel.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
module.exports = function(api) {
22
api.cache(true);
33

4-
const minify = String(process.env.MINIFY) === 'true';
54
const noModules = String(process.env.BABEL_NO_MODULES) === 'true';
65

76
const rename = {};

compat/src/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ declare namespace React {
3939
export import Fragment = preact.Fragment;
4040
export import createElement = preact.createElement;
4141
export import cloneElement = preact.cloneElement;
42+
export import ComponentProps = preact.ComponentProps;
4243

4344
// Suspense
4445
export import Suspense = _Suspense.Suspense;
@@ -105,7 +106,7 @@ declare namespace React {
105106

106107
export function forwardRef<R, P = {}>(
107108
fn: ForwardFn<P, R>
108-
): preact.FunctionalComponent<Omit<P, 'ref'> & { ref?: preact.RefObject<R> }>;
109+
): preact.FunctionalComponent<Omit<P, 'ref'> & { ref?: preact.Ref<R> }>;
109110

110111
export function unstable_batchedUpdates(
111112
callback: (arg?: any) => void,

compat/src/render.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export const REACT_ELEMENT_TYPE =
1010
(typeof Symbol != 'undefined' && Symbol.for && Symbol.for('react.element')) ||
1111
0xeac7;
1212

13-
const CAMEL_PROPS = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
13+
const CAMEL_PROPS = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
14+
15+
const IS_DOM = typeof document !== 'undefined';
1416

1517
// Input types for which onchange should not be converted to oninput.
1618
// type="file|checkbox|radio", plus "range" in IE11.
@@ -112,12 +114,17 @@ options.vnode = vnode => {
112114

113115
// only normalize props on Element nodes
114116
if (typeof type === 'string') {
117+
const nonCustomElement = type.indexOf('-') === -1;
115118
normalizedProps = {};
116119

117120
for (let i in props) {
118121
let value = props[i];
119122

120-
if (i === 'value' && 'defaultValue' in props && value == null) {
123+
if (IS_DOM && i === 'children' && type === 'noscript') {
124+
// Emulate React's behavior of not rendering the contents of noscript tags on the client.
125+
continue;
126+
}
127+
else if (i === 'value' && 'defaultValue' in props && value == null) {
121128
// Skip applying value if it is null/undefined and we already set
122129
// a default value
123130
continue;
@@ -145,7 +152,7 @@ options.vnode = vnode => {
145152
i = 'oninput';
146153
} else if (/^on(Ani|Tra|Tou|BeforeInp)/.test(i)) {
147154
i = i.toLowerCase();
148-
} else if (CAMEL_PROPS.test(i)) {
155+
} else if (nonCustomElement && CAMEL_PROPS.test(i)) {
149156
i = i.replace(/[A-Z0-9]/, '-$&').toLowerCase();
150157
} else if (value === null) {
151158
value = undefined;
@@ -181,12 +188,12 @@ options.vnode = vnode => {
181188
}
182189

183190
vnode.props = normalizedProps;
184-
}
185191

186-
if (type && props.class != props.className) {
187-
classNameDescriptor.enumerable = 'className' in props;
188-
if (props.className != null) normalizedProps.class = props.className;
189-
Object.defineProperty(normalizedProps, 'className', classNameDescriptor);
192+
if (props.class != props.className) {
193+
classNameDescriptor.enumerable = 'className' in props;
194+
if (props.className != null) normalizedProps.class = props.className;
195+
Object.defineProperty(normalizedProps, 'className', classNameDescriptor);
196+
}
190197
}
191198

192199
vnode.$$typeof = REACT_ELEMENT_TYPE;

compat/test/browser/render.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,33 @@ describe('compat render', () => {
217217
expect(document.activeElement.nodeName).to.equal('INPUT');
218218
});
219219

220+
it('should transform react-style camel cased attributes', () => {
221+
render(
222+
<text dominantBaseline="middle" fontWeight="30px">
223+
foo
224+
</text>,
225+
scratch
226+
);
227+
expect(scratch.innerHTML).to.equal(
228+
'<text dominant-baseline="middle" font-weight="30px">foo</text>'
229+
);
230+
});
231+
232+
it('should correctly allow for "className"', () => {
233+
const Foo = props => {
234+
const { className, ...rest } = props;
235+
return (
236+
<div class={className}>
237+
<p {...rest}>Foo</p>
238+
</div>
239+
);
240+
};
241+
242+
render(<Foo className="foo" />, scratch);
243+
expect(scratch.firstChild.className).to.equal('foo');
244+
expect(scratch.firstChild.firstChild.className).to.equal('');
245+
});
246+
220247
it('should normalize class+className even on components', () => {
221248
function Foo(props) {
222249
return (

devtools/src/devtools.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { options, Fragment, Component } from 'preact';
22

33
export function initDevTools() {
44
if (typeof window != 'undefined' && window.__PREACT_DEVTOOLS__) {
5-
window.__PREACT_DEVTOOLS__.attachPreact('10.5.14', options, {
5+
window.__PREACT_DEVTOOLS__.attachPreact('10.6.2', options, {
66
Fragment,
77
Component
88
});

hooks/src/index.d.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PreactContext, Ref as PreactRef, RefObject } from '../..';
1+
import { PreactContext, Ref as PreactRef } from '../..';
22

33
type Inputs = ReadonlyArray<unknown>;
44

@@ -46,8 +46,14 @@ export function useReducer<S, A, I>(
4646
): [S, (action: A) => void];
4747

4848
/** @deprecated Use the `Ref` type instead. */
49-
type PropRef<T> = { current: T };
50-
type Ref<T> = { current: T };
49+
type PropRef<T> = MutableRef<T>;
50+
interface Ref<T> {
51+
readonly current: T | null;
52+
}
53+
54+
interface MutableRef<T> {
55+
current: T;
56+
}
5157

5258
/**
5359
* `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
@@ -58,9 +64,9 @@ type Ref<T> = { current: T };
5864
*
5965
* @param initialValue the initial value to store in the ref object
6066
*/
61-
export function useRef<T>(initialValue: null): RefObject<T>;
62-
export function useRef<T>(initialValue: T): Ref<T>;
63-
export function useRef<T>(): Ref<T | undefined>;
67+
export function useRef<T>(initialValue: T): MutableRef<T>;
68+
export function useRef<T>(initialValue: T | null): Ref<T>;
69+
export function useRef<T = undefined>(): MutableRef<T | undefined>;
6470

6571
type EffectCallback = () => void | (() => void);
6672
/**

hooks/src/index.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@ let currentIndex;
55

66
/** @type {import('./internal').Component} */
77
let currentComponent;
8-
/**
9-
* Keep track of the previous component so that we can set
10-
* `currentComponent` to `null` and throw when a hook is invoked
11-
* outside of render
12-
* @type {import('./internal').Component}
13-
*/
14-
let previousComponent;
158

169
/** @type {number} */
1710
let currentHook = 0;
@@ -54,7 +47,7 @@ options.diffed = vnode => {
5447
if (c && c.__hooks && c.__hooks._pendingEffects.length) {
5548
afterPaint(afterPaintEffects.push(c));
5649
}
57-
currentComponent = previousComponent;
50+
currentComponent = null;
5851
};
5952

6053
options._commit = (vnode, commitQueue) => {
@@ -81,11 +74,15 @@ options.unmount = vnode => {
8174

8275
const c = vnode._component;
8376
if (c && c.__hooks) {
84-
try {
85-
c.__hooks._list.forEach(invokeCleanup);
86-
} catch (e) {
87-
options._catchError(e, c._vnode);
88-
}
77+
let hasErrored;
78+
c.__hooks._list.forEach(s => {
79+
try {
80+
invokeCleanup(s);
81+
} catch (e) {
82+
hasErrored = e;
83+
}
84+
});
85+
if (hasErrored) options._catchError(hasErrored, c._vnode);
8986
}
9087
};
9188

@@ -353,7 +350,11 @@ function invokeCleanup(hook) {
353350
// A hook cleanup can introduce a call to render which creates a new root, this will call options.vnode
354351
// and move the currentComponent away.
355352
const comp = currentComponent;
356-
if (typeof hook._cleanup == 'function') hook._cleanup();
353+
let cleanup = hook._cleanup;
354+
if (typeof cleanup == 'function') {
355+
hook._cleanup = undefined;
356+
cleanup();
357+
}
357358
currentComponent = comp;
358359
}
359360

hooks/test/browser/useEffect.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,4 +370,74 @@ describe('useEffect', () => {
370370
'<div><div>Count: 2</div><div><div>dummy</div></div></div>'
371371
);
372372
});
373+
374+
it('handles errors correctly', () => {
375+
class ErrorBoundary extends Component {
376+
constructor(props) {
377+
super(props);
378+
this.state = { error: null };
379+
}
380+
381+
componentDidCatch(error) {
382+
this.setState({ error: 'oh no' });
383+
}
384+
385+
render() {
386+
return this.state.error ? (
387+
<h2>Error! {this.state.error}</h2>
388+
) : (
389+
this.props.children
390+
);
391+
}
392+
}
393+
394+
let update;
395+
const firstEffectSpy = sinon.spy();
396+
const firstEffectcleanup = sinon.spy();
397+
const secondEffectSpy = sinon.spy();
398+
const secondEffectcleanup = sinon.spy();
399+
400+
const MainContent = () => {
401+
const [val, setVal] = useState(false);
402+
403+
update = () => setVal(!val);
404+
useEffect(() => {
405+
firstEffectSpy();
406+
return () => {
407+
firstEffectcleanup();
408+
throw new Error('oops');
409+
};
410+
}, [val]);
411+
412+
useEffect(() => {
413+
secondEffectSpy();
414+
return () => {
415+
secondEffectcleanup();
416+
};
417+
}, []);
418+
419+
return <h1>Hello world</h1>;
420+
};
421+
422+
act(() => {
423+
render(
424+
<ErrorBoundary>
425+
<MainContent />
426+
</ErrorBoundary>,
427+
scratch
428+
);
429+
});
430+
431+
expect(firstEffectSpy).to.be.calledOnce;
432+
expect(secondEffectSpy).to.be.calledOnce;
433+
434+
act(() => {
435+
update();
436+
});
437+
438+
expect(firstEffectSpy).to.be.calledOnce;
439+
expect(secondEffectSpy).to.be.calledOnce;
440+
expect(firstEffectcleanup).to.be.calledOnce;
441+
expect(secondEffectcleanup).to.be.calledOnce;
442+
});
373443
});

karma.conf.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ var coverage = String(process.env.COVERAGE) === 'true',
44
minify = String(process.env.MINIFY) === 'true',
55
ci = String(process.env.CI).match(/^(1|true)$/gi),
66
sauceLabs = ci && String(process.env.RUN_SAUCE_LABS) === 'true',
7+
// always downlevel to ES5 for saucelabs:
8+
downlevel = sauceLabs || String(process.env.DOWNLEVEL) === 'true',
79
performance = !coverage && String(process.env.PERFORMANCE) !== 'false',
810
path = require('path'),
911
errorstacks = require('errorstacks'),
@@ -163,7 +165,7 @@ function createEsbuildPlugin() {
163165
loose: true,
164166
modules: false,
165167
targets: {
166-
browsers: ['last 2 versions', 'IE >= 9']
168+
browsers: ['last 2 versions', 'IE >= 11']
167169
}
168170
}
169171
]
@@ -178,7 +180,7 @@ function createEsbuildPlugin() {
178180
});
179181

180182
// Apply babel pass whenever we load a .js file
181-
build.onLoad({ filter: /\.js$/ }, async args => {
183+
build.onLoad({ filter: /\.[mc]?js$/ }, async args => {
182184
const contents = await fs.readFile(args.path, 'utf-8');
183185

184186
// Using a cache is crucial as babel is 30x slower than esbuild
@@ -202,6 +204,20 @@ function createEsbuildPlugin() {
202204
const tmp = await babel.transformAsync(result, {
203205
filename: args.path,
204206
sourceMaps: 'inline',
207+
presets: downlevel
208+
? [
209+
[
210+
'@babel/preset-env',
211+
{
212+
loose: true,
213+
modules: false,
214+
targets: {
215+
browsers: ['last 2 versions', 'IE >= 11']
216+
}
217+
}
218+
]
219+
]
220+
: [],
205221
plugins: [
206222
coverage && [
207223
'istanbul',
@@ -326,7 +342,7 @@ module.exports = function(config) {
326342
singleBundle: false,
327343

328344
// esbuild options
329-
target: 'es5',
345+
target: downlevel ? 'es5' : 'es2015',
330346
define: {
331347
COVERAGE: coverage,
332348
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || ''),

mangle.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,4 @@
7777
"$_isSuspended": "__i"
7878
}
7979
}
80-
}
80+
}

0 commit comments

Comments
 (0)