diff --git a/.changeset/lazy-walls-fold.md b/.changeset/lazy-walls-fold.md
new file mode 100644
index 00000000..40394bf1
--- /dev/null
+++ b/.changeset/lazy-walls-fold.md
@@ -0,0 +1,5 @@
+---
+'@prefresh/core': patch
+---
+
+Prevent double HOC's from duplicating components by having the types match up for Preact
diff --git a/packages/core/src/constants.js b/packages/core/src/constants.js
index b39d4037..004fde48 100644
--- a/packages/core/src/constants.js
+++ b/packages/core/src/constants.js
@@ -11,3 +11,4 @@ export const VNODE_CHILDREN = '__k';
export const HOOK_VALUE = '__';
export const HOOK_ARGS = '__H';
export const HOOK_CLEANUP = '__c';
+export const VNODE_PARENT = '__';
diff --git a/packages/core/src/index.js b/packages/core/src/index.js
index 1a90ffc9..79338d72 100644
--- a/packages/core/src/index.js
+++ b/packages/core/src/index.js
@@ -17,6 +17,7 @@ import {
HOOK_ARGS,
HOOK_VALUE,
HOOK_CLEANUP,
+ VNODE_PARENT,
} from './constants';
import { computeKey } from './computeKey';
import { vnodesForComponent, mappedVNodes } from './runtime/vnodesForComponent';
@@ -56,11 +57,9 @@ function replaceComponent(OldType, NewType, resetHookState) {
pendingUpdates = pendingUpdates.filter(p => p[0] !== OldType);
vnodes.forEach(vnode => {
- // update the type in-place to reference the new component
vnode.type = NewType;
-
if (vnode[VNODE_COMPONENT]) {
- vnode[VNODE_COMPONENT].constructor = vnode.type;
+ vnode[VNODE_COMPONENT].constructor = NewType;
try {
if (vnode[VNODE_COMPONENT] instanceof OldType) {
@@ -99,6 +98,22 @@ function replaceComponent(OldType, NewType, resetHookState) {
vnode[VNODE_COMPONENT].constructor = NewType;
}
+ if (
+ vnode[VNODE_PARENT] &&
+ vnode[VNODE_PARENT][VNODE_CHILDREN] &&
+ vnode[VNODE_PARENT][VNODE_CHILDREN].length
+ ) {
+ vnode[VNODE_PARENT][VNODE_CHILDREN].forEach(child => {
+ if (
+ child &&
+ typeof child.type === 'function' &&
+ child.type === OldType
+ ) {
+ child.type = NewType;
+ }
+ });
+ }
+
if (resetHookState) {
if (
vnode[VNODE_COMPONENT][COMPONENT_HOOKS] &&
@@ -164,16 +179,11 @@ function replaceComponent(OldType, NewType, resetHookState) {
}
);
- vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST].forEach(
- hook => {
- if (
- hook.__H &&
- Array.isArray(hook.__H)
- ) {
- hook.__H = undefined;
- }
+ vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST].forEach(hook => {
+ if (hook.__H && Array.isArray(hook.__H)) {
+ hook.__H = undefined;
}
- );
+ });
}
}
diff --git a/test/constants.js b/test/constants.js
index 9d60e0d8..2b87d6aa 100644
--- a/test/constants.js
+++ b/test/constants.js
@@ -33,7 +33,7 @@ exports.binArgs = {
};
exports.goMessage = {
- vite: 'running',
+ vite: 'ready',
snowpack: 'Server started',
webpack: 'successfully',
nollup: 'Compiled',
diff --git a/test/fixture/next-webpack5/src/app.jsx b/test/fixture/next-webpack5/src/app.jsx
index 18cd8ac9..768c987e 100644
--- a/test/fixture/next-webpack5/src/app.jsx
+++ b/test/fixture/next-webpack5/src/app.jsx
@@ -1,11 +1,12 @@
-import { useCounter } from './useCounter'
import { h } from 'preact'
+import { setup } from 'goober';
import { Greeting } from './greeting.jsx';
import { StoreProvider } from './context.jsx';
import { Products } from './products.jsx';
import { Effect } from './effect.jsx';
+// import { List } from './list.jsx';
+import { useCounter } from './useCounter'
import { Style } from './styles';
-import { setup } from 'goober';
setup(h);
@@ -28,6 +29,7 @@ export function App(props) {
+ {/*
*/}
)
}
diff --git a/test/fixture/next-webpack5/src/hoc.jsx b/test/fixture/next-webpack5/src/hoc.jsx
new file mode 100644
index 00000000..1a47a727
--- /dev/null
+++ b/test/fixture/next-webpack5/src/hoc.jsx
@@ -0,0 +1,3 @@
+import { h } from 'preact';
+
+export const applyHOC = (Component) => (props) =>
diff --git a/test/fixture/next-webpack5/src/list.jsx b/test/fixture/next-webpack5/src/list.jsx
new file mode 100644
index 00000000..f5963801
--- /dev/null
+++ b/test/fixture/next-webpack5/src/list.jsx
@@ -0,0 +1,8 @@
+import { h } from 'preact'
+import Item from './listItem.jsx'
+
+const items = [0, 1, 2, 3]
+
+export const List = () => {
+ return
{items.map(item => - )}
+}
diff --git a/test/fixture/next-webpack5/src/listItem.jsx b/test/fixture/next-webpack5/src/listItem.jsx
new file mode 100644
index 00000000..eccc72f3
--- /dev/null
+++ b/test/fixture/next-webpack5/src/listItem.jsx
@@ -0,0 +1,12 @@
+import { h, Component } from 'preact'
+import { applyHOC } from './hoc.jsx'
+
+class ListItem extends Component {
+ render() {
+ return item {this.props.index}
+ }
+}
+
+const WrappedListItem = applyHOC(ListItem)
+
+export default WrappedListItem
diff --git a/test/fixture/next/src/app.jsx b/test/fixture/next/src/app.jsx
index 18cd8ac9..66d62e44 100644
--- a/test/fixture/next/src/app.jsx
+++ b/test/fixture/next/src/app.jsx
@@ -1,11 +1,12 @@
-import { useCounter } from './useCounter'
+import { setup } from 'goober';
import { h } from 'preact'
import { Greeting } from './greeting.jsx';
import { StoreProvider } from './context.jsx';
import { Products } from './products.jsx';
import { Effect } from './effect.jsx';
+// import { List } from './list.jsx';
import { Style } from './styles';
-import { setup } from 'goober';
+import { useCounter } from './useCounter';
setup(h);
@@ -28,6 +29,7 @@ export function App(props) {
+ {/*
*/}
)
}
diff --git a/test/fixture/next/src/hoc.jsx b/test/fixture/next/src/hoc.jsx
new file mode 100644
index 00000000..1a47a727
--- /dev/null
+++ b/test/fixture/next/src/hoc.jsx
@@ -0,0 +1,3 @@
+import { h } from 'preact';
+
+export const applyHOC = (Component) => (props) =>
diff --git a/test/fixture/next/src/list.jsx b/test/fixture/next/src/list.jsx
new file mode 100644
index 00000000..f5963801
--- /dev/null
+++ b/test/fixture/next/src/list.jsx
@@ -0,0 +1,8 @@
+import { h } from 'preact'
+import Item from './listItem.jsx'
+
+const items = [0, 1, 2, 3]
+
+export const List = () => {
+ return {items.map(item => - )}
+}
diff --git a/test/fixture/next/src/listItem.jsx b/test/fixture/next/src/listItem.jsx
new file mode 100644
index 00000000..eccc72f3
--- /dev/null
+++ b/test/fixture/next/src/listItem.jsx
@@ -0,0 +1,12 @@
+import { h, Component } from 'preact'
+import { applyHOC } from './hoc.jsx'
+
+class ListItem extends Component {
+ render() {
+ return item {this.props.index}
+ }
+}
+
+const WrappedListItem = applyHOC(ListItem)
+
+export default WrappedListItem
diff --git a/test/fixture/nollup/src/app.jsx b/test/fixture/nollup/src/app.jsx
index 78e2bde2..a9ebfe91 100644
--- a/test/fixture/nollup/src/app.jsx
+++ b/test/fixture/nollup/src/app.jsx
@@ -1,11 +1,12 @@
import { h } from 'preact';
-import { useCounter } from './useCounter'
+import { setup } from 'goober';
import { Greeting } from './greeting.jsx';
import { StoreProvider } from './context.jsx';
import { Products } from './products.jsx';
import { Effect } from './effect.jsx';
+// import { List } from './list.jsx';
import { Style } from './styles';
-import { setup } from 'goober';
+import { useCounter } from './useCounter'
setup(h);
@@ -28,6 +29,7 @@ export function App(props) {
+ {/*
*/}
)
}
diff --git a/test/fixture/nollup/src/hoc.jsx b/test/fixture/nollup/src/hoc.jsx
new file mode 100644
index 00000000..1a47a727
--- /dev/null
+++ b/test/fixture/nollup/src/hoc.jsx
@@ -0,0 +1,3 @@
+import { h } from 'preact';
+
+export const applyHOC = (Component) => (props) =>
diff --git a/test/fixture/nollup/src/list.jsx b/test/fixture/nollup/src/list.jsx
new file mode 100644
index 00000000..f5963801
--- /dev/null
+++ b/test/fixture/nollup/src/list.jsx
@@ -0,0 +1,8 @@
+import { h } from 'preact'
+import Item from './listItem.jsx'
+
+const items = [0, 1, 2, 3]
+
+export const List = () => {
+ return {items.map(item => - )}
+}
diff --git a/test/fixture/nollup/src/listItem.jsx b/test/fixture/nollup/src/listItem.jsx
new file mode 100644
index 00000000..eccc72f3
--- /dev/null
+++ b/test/fixture/nollup/src/listItem.jsx
@@ -0,0 +1,12 @@
+import { h, Component } from 'preact'
+import { applyHOC } from './hoc.jsx'
+
+class ListItem extends Component {
+ render() {
+ return item {this.props.index}
+ }
+}
+
+const WrappedListItem = applyHOC(ListItem)
+
+export default WrappedListItem
diff --git a/test/fixture/snowpack/src/app.jsx b/test/fixture/snowpack/src/app.jsx
index d3ccd217..bea808dc 100644
--- a/test/fixture/snowpack/src/app.jsx
+++ b/test/fixture/snowpack/src/app.jsx
@@ -1,11 +1,12 @@
-import { useCounter } from './useCounter'
import { h } from 'preact'
+import { setup } from 'goober';
import { StoreProvider } from './context';
import { Products } from './products';
import { Greeting } from './greeting';
import { Effect } from './effect';
-import { setup } from 'goober';
import { Style } from './styles';
+// import { List } from './list';
+import { useCounter } from './useCounter'
setup(h);
@@ -28,6 +29,7 @@ export function App(props) {
+ {/*
*/}
)
}
diff --git a/test/fixture/snowpack/src/hoc.jsx b/test/fixture/snowpack/src/hoc.jsx
new file mode 100644
index 00000000..1a47a727
--- /dev/null
+++ b/test/fixture/snowpack/src/hoc.jsx
@@ -0,0 +1,3 @@
+import { h } from 'preact';
+
+export const applyHOC = (Component) => (props) =>
diff --git a/test/fixture/snowpack/src/list.jsx b/test/fixture/snowpack/src/list.jsx
new file mode 100644
index 00000000..f5963801
--- /dev/null
+++ b/test/fixture/snowpack/src/list.jsx
@@ -0,0 +1,8 @@
+import { h } from 'preact'
+import Item from './listItem.jsx'
+
+const items = [0, 1, 2, 3]
+
+export const List = () => {
+ return {items.map(item => - )}
+}
diff --git a/test/fixture/snowpack/src/listItem.jsx b/test/fixture/snowpack/src/listItem.jsx
new file mode 100644
index 00000000..eccc72f3
--- /dev/null
+++ b/test/fixture/snowpack/src/listItem.jsx
@@ -0,0 +1,12 @@
+import { h, Component } from 'preact'
+import { applyHOC } from './hoc.jsx'
+
+class ListItem extends Component {
+ render() {
+ return item {this.props.index}
+ }
+}
+
+const WrappedListItem = applyHOC(ListItem)
+
+export default WrappedListItem
diff --git a/test/fixture/vite/src/app.jsx b/test/fixture/vite/src/app.jsx
index 60d59f68..7c900bc4 100644
--- a/test/fixture/vite/src/app.jsx
+++ b/test/fixture/vite/src/app.jsx
@@ -1,11 +1,12 @@
import { h } from 'preact';
-import { useCounter } from './useCounter';
+import { setup } from 'goober';
import { StoreProvider } from './context';
import { Products } from './products';
import { Greeting } from './greeting';
import { Effect } from './effect';
-import { setup } from 'goober';
import { Style } from './styles';
+import { List } from './list';
+import { useCounter } from './useCounter';
setup(h);
@@ -28,6 +29,7 @@ export function App(props) {
+
)
}
diff --git a/test/fixture/vite/src/hoc.jsx b/test/fixture/vite/src/hoc.jsx
new file mode 100644
index 00000000..1a47a727
--- /dev/null
+++ b/test/fixture/vite/src/hoc.jsx
@@ -0,0 +1,3 @@
+import { h } from 'preact';
+
+export const applyHOC = (Component) => (props) =>
diff --git a/test/fixture/vite/src/list.jsx b/test/fixture/vite/src/list.jsx
new file mode 100644
index 00000000..f5963801
--- /dev/null
+++ b/test/fixture/vite/src/list.jsx
@@ -0,0 +1,8 @@
+import { h } from 'preact'
+import Item from './listItem.jsx'
+
+const items = [0, 1, 2, 3]
+
+export const List = () => {
+ return {items.map(item => - )}
+}
diff --git a/test/fixture/vite/src/listItem.jsx b/test/fixture/vite/src/listItem.jsx
new file mode 100644
index 00000000..eccc72f3
--- /dev/null
+++ b/test/fixture/vite/src/listItem.jsx
@@ -0,0 +1,12 @@
+import { h, Component } from 'preact'
+import { applyHOC } from './hoc.jsx'
+
+class ListItem extends Component {
+ render() {
+ return item {this.props.index}
+ }
+}
+
+const WrappedListItem = applyHOC(ListItem)
+
+export default WrappedListItem
diff --git a/test/fixture/web-dev-server/src/app.jsx b/test/fixture/web-dev-server/src/app.jsx
index 25e52855..57694795 100644
--- a/test/fixture/web-dev-server/src/app.jsx
+++ b/test/fixture/web-dev-server/src/app.jsx
@@ -1,11 +1,12 @@
import { h } from 'preact';
+import { setup } from 'goober';
import { useCounter } from './useCounter'
import { StoreProvider } from './context';
import { Products } from './products'
import { Greeting } from './greeting';
import { Effect } from './effect';
-import { setup } from 'goober';
import { Style } from './styles';
+// import { List } from './list';
setup(h);
@@ -28,6 +29,7 @@ export function App(props) {
+ {/*
*/}
)
}
diff --git a/test/fixture/web-dev-server/src/hoc.jsx b/test/fixture/web-dev-server/src/hoc.jsx
new file mode 100644
index 00000000..1a47a727
--- /dev/null
+++ b/test/fixture/web-dev-server/src/hoc.jsx
@@ -0,0 +1,3 @@
+import { h } from 'preact';
+
+export const applyHOC = (Component) => (props) =>
diff --git a/test/fixture/web-dev-server/src/list.jsx b/test/fixture/web-dev-server/src/list.jsx
new file mode 100644
index 00000000..f5963801
--- /dev/null
+++ b/test/fixture/web-dev-server/src/list.jsx
@@ -0,0 +1,8 @@
+import { h } from 'preact'
+import Item from './listItem.jsx'
+
+const items = [0, 1, 2, 3]
+
+export const List = () => {
+ return {items.map(item => - )}
+}
diff --git a/test/fixture/web-dev-server/src/listItem.jsx b/test/fixture/web-dev-server/src/listItem.jsx
new file mode 100644
index 00000000..eccc72f3
--- /dev/null
+++ b/test/fixture/web-dev-server/src/listItem.jsx
@@ -0,0 +1,12 @@
+import { h, Component } from 'preact'
+import { applyHOC } from './hoc.jsx'
+
+class ListItem extends Component {
+ render() {
+ return item {this.props.index}
+ }
+}
+
+const WrappedListItem = applyHOC(ListItem)
+
+export default WrappedListItem
diff --git a/test/fixture/webpack/src/app.jsx b/test/fixture/webpack/src/app.jsx
index 5d2e4675..2a718401 100644
--- a/test/fixture/webpack/src/app.jsx
+++ b/test/fixture/webpack/src/app.jsx
@@ -1,11 +1,12 @@
-import { useCounter } from './useCounter'
import { h } from 'preact'
+import { setup } from 'goober';
+import { List } from './list.jsx';
import { Greeting } from './greeting.jsx';
import { StoreProvider } from './context.jsx';
import { Products } from './products.jsx';
import { Effect } from './effect.jsx';
import { Style } from './styles';
-import { setup } from 'goober';
+import { useCounter } from './useCounter'
setup(h);
@@ -31,6 +32,7 @@ export function App(props) {
+
)
}
diff --git a/test/fixture/webpack/src/hoc.jsx b/test/fixture/webpack/src/hoc.jsx
new file mode 100644
index 00000000..1a47a727
--- /dev/null
+++ b/test/fixture/webpack/src/hoc.jsx
@@ -0,0 +1,3 @@
+import { h } from 'preact';
+
+export const applyHOC = (Component) => (props) =>
diff --git a/test/fixture/webpack/src/list.jsx b/test/fixture/webpack/src/list.jsx
new file mode 100644
index 00000000..f5963801
--- /dev/null
+++ b/test/fixture/webpack/src/list.jsx
@@ -0,0 +1,8 @@
+import { h } from 'preact'
+import Item from './listItem.jsx'
+
+const items = [0, 1, 2, 3]
+
+export const List = () => {
+ return {items.map(item => - )}
+}
diff --git a/test/fixture/webpack/src/listItem.jsx b/test/fixture/webpack/src/listItem.jsx
new file mode 100644
index 00000000..eccc72f3
--- /dev/null
+++ b/test/fixture/webpack/src/listItem.jsx
@@ -0,0 +1,12 @@
+import { h, Component } from 'preact'
+import { applyHOC } from './hoc.jsx'
+
+class ListItem extends Component {
+ render() {
+ return item {this.props.index}
+ }
+}
+
+const WrappedListItem = applyHOC(ListItem)
+
+export default WrappedListItem
diff --git a/test/index.test.js b/test/index.test.js
index d221f4fb..f4ec8b77 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -26,7 +26,7 @@ describe('Prefresh integrations', () => {
console.log('[BROWSER LOG]: ', msg);
};
- let serverConsoleListener;
+ let serverConsoleListener, serverErrorListener;
async function updateFile(file, replacer) {
const compPath = path.join(getTempDir(integration), file);
@@ -89,12 +89,20 @@ describe('Prefresh integrations', () => {
}
);
- await new Promise(resolve => {
+ await new Promise((resolve, reject) => {
+ const heap = [];
+ const bailTime = setTimeout(() => {
+ console.error(JSON.stringify(heap));
+ throw new Error(`Could not find start command for ${integration}`);
+ }, 30000);
devServer.stdout.on(
'data',
(serverConsoleListener = data => {
console.log('[SERVER LOG]: ', data.toString());
+ heap.push(data.toString());
if (data.toString().match(goMessage[integration])) {
+ console.log('[BOOT]:', integration);
+ clearTimeout(bailTime);
resolve();
}
})
@@ -102,7 +110,7 @@ describe('Prefresh integrations', () => {
devServer.stderr.on(
'data',
- (serverConsoleListener = data => {
+ (serverErrorListener = data => {
console.log('[ERROR SERVER LOG]: ', data.toString());
})
);
@@ -149,7 +157,7 @@ describe('Prefresh integrations', () => {
await fs.writeFile(
compPath,
`import { h } from 'preact';
- export const Tester = () => Test
;`
+ export const Tester = () => Test
;`
);
await updateFile('src/app.jsx', content => {
@@ -304,6 +312,45 @@ describe('Prefresh integrations', () => {
await page.$eval('#color', e => getComputedStyle(e).backgroundColor)
).toBe('rgb(255, 255, 255)');
});
+
+ if (integration === 'vite' || integration === 'webpack') {
+ test('can update in-file HOCs', async () => {
+ let listItems = await page.$('#item-list');
+ let children = await listItems.$$('div');
+
+ expect(children.length).toEqual(4);
+ expect(await getText(children[0])).toMatch('item 0');
+ expect(await getText(children[1])).toMatch('item 1');
+
+ await updateFile('src/listItem.jsx', content =>
+ content.replace(
+ 'item {this.props.index}',
+ 'items {this.props.index}'
+ )
+ );
+ await timeout(TIMEOUT);
+
+ listItems = await page.$('#item-list');
+ children = await listItems.$$('div');
+ expect(children.length).toEqual(4);
+ expect(await getText(children[0])).toMatch('items 0');
+ expect(await getText(children[1])).toMatch('items 1');
+
+ await updateFile('src/listItem.jsx', content =>
+ content.replace(
+ 'items {this.props.index}',
+ 'item {this.props.index} --'
+ )
+ );
+ await timeout(TIMEOUT);
+
+ listItems = await page.$('#item-list');
+ children = await listItems.$$('div');
+ expect(children.length).toEqual(4);
+ expect(await getText(children[0])).toMatch('item 0 --');
+ expect(await getText(children[1])).toMatch('item 1 --');
+ });
+ }
});
});
});