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 --'); + }); + } }); }); });