From c740ac0cb611d4b2979a4b9413bd5abb901e776b Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 26 Feb 2021 09:31:44 +0100 Subject: [PATCH] (wmr) - Fix non js hmr (#370) --- .changeset/fifty-wolves-play.md | 5 + .changeset/light-pens-battle.md | 5 + packages/wmr/src/plugins/wmr/client.js | 23 +++- packages/wmr/src/wmr-middleware.js | 36 ++++-- packages/wmr/test/fixtures.test.js | 119 ++++++++++++++++++ packages/wmr/test/fixtures/hmr-scss/home.scss | 3 + .../wmr/test/fixtures/hmr-scss/index.html | 12 ++ packages/wmr/test/fixtures/hmr-scss/index.js | 7 ++ .../wmr/test/fixtures/hmr-scss/index.scss | 5 + packages/wmr/test/fixtures/hmr/home.js | 5 + packages/wmr/test/fixtures/hmr/index.css | 3 + packages/wmr/test/fixtures/hmr/index.html | 12 ++ packages/wmr/test/fixtures/hmr/index.js | 21 ++++ .../wmr/test/fixtures/hmr/style.module.css | 4 + 14 files changed, 247 insertions(+), 13 deletions(-) create mode 100644 .changeset/fifty-wolves-play.md create mode 100644 .changeset/light-pens-battle.md create mode 100644 packages/wmr/test/fixtures/hmr-scss/home.scss create mode 100644 packages/wmr/test/fixtures/hmr-scss/index.html create mode 100644 packages/wmr/test/fixtures/hmr-scss/index.js create mode 100644 packages/wmr/test/fixtures/hmr-scss/index.scss create mode 100644 packages/wmr/test/fixtures/hmr/home.js create mode 100644 packages/wmr/test/fixtures/hmr/index.css create mode 100644 packages/wmr/test/fixtures/hmr/index.html create mode 100644 packages/wmr/test/fixtures/hmr/index.js create mode 100644 packages/wmr/test/fixtures/hmr/style.module.css diff --git a/.changeset/fifty-wolves-play.md b/.changeset/fifty-wolves-play.md new file mode 100644 index 000000000..f86bc6ba7 --- /dev/null +++ b/.changeset/fifty-wolves-play.md @@ -0,0 +1,5 @@ +--- +'wmr': patch +--- + +Fix non-js hmr diff --git a/.changeset/light-pens-battle.md b/.changeset/light-pens-battle.md new file mode 100644 index 000000000..d26777417 --- /dev/null +++ b/.changeset/light-pens-battle.md @@ -0,0 +1,5 @@ +--- +'wmr': patch +--- + +Fix support for scss/sass hmr diff --git a/packages/wmr/src/plugins/wmr/client.js b/packages/wmr/src/plugins/wmr/client.js index bbe1e8721..8404e8458 100644 --- a/packages/wmr/src/plugins/wmr/client.js +++ b/packages/wmr/src/plugins/wmr/client.js @@ -40,7 +40,7 @@ function handleMessage(e) { url = resolve(url); if (!mods.get(url)) { - if (/\.css$/.test(url)) { + if (/\.(css|s[ac]ss)$/.test(url)) { if (mods.has(url + '.js')) { url += '.js'; } else { @@ -164,14 +164,35 @@ export function style(filename, id) { } } +function traverseSheet(sheet, target) { + for (let i = 0; i < sheet.rules.length; i++) { + if (sheet.rules[i].href && resolve(strip(sheet.rules[i].href)) === strip(target)) { + return sheet.rules[i]; + } else if (sheet.rules[i].styleSheet) { + return traverseSheet(sheet.rules[i].styleSheet, target); + } + } +} + // Update a non-imported stylesheet function updateStyleSheet(url) { const sheets = document.styleSheets; + for (let i = 0; i < sheets.length; i++) { if (sheets[i].href && strip(sheets[i].href) === url) { // @ts-ignore sheets[i].ownerNode.href = strip(url) + '?t=' + Date.now(); return true; } + + const found = traverseSheet(sheets[i], url); + if (found) { + const index = [].indexOf.call(found.parentStyleSheet.rules, found); + const urlStr = JSON.stringify(strip(url) + '?t=' + Date.now()); + const css = found.cssText.replace(/^(@import|@use)\s*(?:url\([^)]*\)|(['"]).*?\2)/, '$1 ' + urlStr); + found.parentStyleSheet.insertRule(css, index); + found.parentStyleSheet.deleteRule(index + 1); + return true; + } } } diff --git a/packages/wmr/src/wmr-middleware.js b/packages/wmr/src/wmr-middleware.js index df8c14fa9..cbb5d395e 100644 --- a/packages/wmr/src/wmr-middleware.js +++ b/packages/wmr/src/wmr-middleware.js @@ -117,12 +117,16 @@ export default function wmrMiddleware({ }); const pendingChanges = new Set(); + let timeout = null; function flushChanges() { + timeout = null; onChange({ changes: Array.from(pendingChanges), duration: 0 }); pendingChanges.clear(); } - function bubbleUpdates(filename) { + function bubbleUpdates(filename, visited = new Set()) { + if (visited.has(filename)) return true; + visited.add(filename); // Delete file from the in-memory cache: WRITE_CACHE.delete(filename); @@ -133,17 +137,15 @@ export default function wmrMiddleware({ if (mod.acceptingUpdates) { pendingChanges.add(filename); + return true; } else if (mod.dependents.size) { - mod.dependents.forEach(function (value) { + return mod.dependents.every(function (value) { mod.stale = true; - bubbleUpdates(value); + return bubbleUpdates(value, visited); }); - } else { - // We need a full-reload signal - return false; } - - return true; + // We need a full-reload signal + return false; } watcher.on('change', filename => { @@ -154,10 +156,20 @@ export default function wmrMiddleware({ // Delete any generated CSS Modules mapping modules: if (/\.module\.css$/.test(filename)) WRITE_CACHE.delete(filename + '.js'); - if (!pendingChanges.size) setTimeout(flushChanges, 60); - const result = bubbleUpdates(filename); - if (!result) { - onChange({ type: 'reload' }); + if (!pendingChanges.size) timeout = setTimeout(flushChanges, 60); + + if (/\.(css|s[ac]ss)$/.test(filename)) { + WRITE_CACHE.delete(filename); + pendingChanges.add('/' + filename); + } else if (/\.(mjs|[tj]sx?)$/.test(filename)) { + if (!bubbleUpdates(filename)) { + clearTimeout(timeout); + onChange({ reload: true }); + } + } else { + WRITE_CACHE.delete(filename); + clearTimeout(timeout); + onChange({ reload: true }); } }); diff --git a/packages/wmr/test/fixtures.test.js b/packages/wmr/test/fixtures.test.js index 7ec612811..faed0ee78 100644 --- a/packages/wmr/test/fixtures.test.js +++ b/packages/wmr/test/fixtures.test.js @@ -194,6 +194,125 @@ describe('fixtures', () => { }); }); + describe('hmr', () => { + async function updateFile(tempDir, file, replacer) { + const compPath = path.join(tempDir, file); + const content = await fs.readFile(compPath, 'utf-8'); + await fs.writeFile(compPath, replacer(content)); + } + + const timeout = n => new Promise(r => setTimeout(r, n)); + + it('should hot reload the child-file', async () => { + await loadFixture('hmr', env); + instance = await runWmrFast(env.tmp.path); + await getOutput(env, instance); + + const home = await env.page.$('.home'); + let text = home ? await home.evaluate(el => el.textContent) : null; + expect(text).toEqual('Home'); + + await updateFile(env.tmp.path, 'home.js', content => + content.replace('

Home

', '

Away

') + ); + + await timeout(1000); + + text = home ? await home.evaluate(el => el.textContent) : null; + expect(text).toEqual('Away'); + }); + + it('should hot reload for a newly created file', async () => { + await loadFixture('hmr', env); + instance = await runWmrFast(env.tmp.path); + await getOutput(env, instance); + + const compPath = path.join(env.tmp.path, 'child.js'); + await fs.writeFile(compPath, `export default function Child() {return

child

}`); + + const home = await env.page.$('.home'); + let text = home ? await home.evaluate(el => el.textContent) : null; + expect(text).toEqual('Home'); + + await updateFile(env.tmp.path, 'home.js', content => { + const newContent = `import Child from './child.js';\n\n${content}`; + return newContent.replace('

Home

', ''); + }); + + await timeout(1000); + + const child = await env.page.$('.child'); + text = child ? await child.evaluate(el => el.textContent) : null; + expect(text).toEqual('child'); + }); + + it('should hot reload a css-file imported from index.html', async () => { + await loadFixture('hmr', env); + instance = await runWmrFast(env.tmp.path); + await getOutput(env, instance); + + expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); + + await updateFile(env.tmp.path, 'index.css', content => content.replace('color: #333;', 'color: #000;')); + + await timeout(1000); + + expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 0)'); + }); + + it('should hot reload a module css-file', async () => { + await loadFixture('hmr', env); + instance = await runWmrFast(env.tmp.path); + await getOutput(env, instance); + + expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); + + await updateFile(env.tmp.path, 'style.module.css', content => content.replace('color: #333;', 'color: #000;')); + + await timeout(1000); + + expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 0)'); + }); + }); + + describe('hmr-scss', () => { + async function updateFile(tempDir, file, replacer) { + const compPath = path.join(tempDir, file); + const content = await fs.readFile(compPath, 'utf-8'); + await fs.writeFile(compPath, replacer(content)); + } + + const timeout = n => new Promise(r => setTimeout(r, n)); + + it('should hot reload an scss-file imported from index.html', async () => { + await loadFixture('hmr-scss', env); + instance = await runWmrFast(env.tmp.path); + await getOutput(env, instance); + + expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); + + await updateFile(env.tmp.path, 'index.scss', content => content.replace('color: #333;', 'color: #000;')); + + await timeout(1000); + + expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 0)'); + }); + + it('should hot reload an imported scss-file from another scss-file', async () => { + await loadFixture('hmr-scss', env); + instance = await runWmrFast(env.tmp.path); + await getOutput(env, instance); + + expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); + + await updateFile(env.tmp.path, 'home.scss', content => content.replace('color: #333;', 'color: #000;')); + + await timeout(1000); + + expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 0)'); + }); + }); + describe('commonjs', () => { it('should transpile .cjs files', async () => { await loadFixture('commonjs', env); diff --git a/packages/wmr/test/fixtures/hmr-scss/home.scss b/packages/wmr/test/fixtures/hmr-scss/home.scss new file mode 100644 index 000000000..289d630fa --- /dev/null +++ b/packages/wmr/test/fixtures/hmr-scss/home.scss @@ -0,0 +1,3 @@ +main { + color: #333; +} diff --git a/packages/wmr/test/fixtures/hmr-scss/index.html b/packages/wmr/test/fixtures/hmr-scss/index.html new file mode 100644 index 000000000..d353532c9 --- /dev/null +++ b/packages/wmr/test/fixtures/hmr-scss/index.html @@ -0,0 +1,12 @@ + + + + + Simple + + + +
+ + + diff --git a/packages/wmr/test/fixtures/hmr-scss/index.js b/packages/wmr/test/fixtures/hmr-scss/index.js new file mode 100644 index 000000000..d1f92488d --- /dev/null +++ b/packages/wmr/test/fixtures/hmr-scss/index.js @@ -0,0 +1,7 @@ +import { render } from 'preact'; + +export function App() { + return
Test
; +} + +render(, document.body); diff --git a/packages/wmr/test/fixtures/hmr-scss/index.scss b/packages/wmr/test/fixtures/hmr-scss/index.scss new file mode 100644 index 000000000..021123bd1 --- /dev/null +++ b/packages/wmr/test/fixtures/hmr-scss/index.scss @@ -0,0 +1,5 @@ +@import 'home.scss'; + +body { + color: #333; +} diff --git a/packages/wmr/test/fixtures/hmr/home.js b/packages/wmr/test/fixtures/hmr/home.js new file mode 100644 index 000000000..bb1353477 --- /dev/null +++ b/packages/wmr/test/fixtures/hmr/home.js @@ -0,0 +1,5 @@ +function Home() { + return

Home

; +} + +export default Home; diff --git a/packages/wmr/test/fixtures/hmr/index.css b/packages/wmr/test/fixtures/hmr/index.css new file mode 100644 index 000000000..ecb909093 --- /dev/null +++ b/packages/wmr/test/fixtures/hmr/index.css @@ -0,0 +1,3 @@ +body { + color: #333; +} diff --git a/packages/wmr/test/fixtures/hmr/index.html b/packages/wmr/test/fixtures/hmr/index.html new file mode 100644 index 000000000..deae5ac77 --- /dev/null +++ b/packages/wmr/test/fixtures/hmr/index.html @@ -0,0 +1,12 @@ + + + + + Simple + + + +
+ + + diff --git a/packages/wmr/test/fixtures/hmr/index.js b/packages/wmr/test/fixtures/hmr/index.js new file mode 100644 index 000000000..ff99220b9 --- /dev/null +++ b/packages/wmr/test/fixtures/hmr/index.js @@ -0,0 +1,21 @@ +import { render } from 'preact'; +import styles from './style.module.css'; +import Home from './home.js'; + +export function App() { + return ( +
+
+ +
+ +
+ ); +} + +render(, document.body); + +// @ts-ignore +if (module.hot) module.hot.accept(u => render(, document.body)); diff --git a/packages/wmr/test/fixtures/hmr/style.module.css b/packages/wmr/test/fixtures/hmr/style.module.css new file mode 100644 index 000000000..9b4c5cb18 --- /dev/null +++ b/packages/wmr/test/fixtures/hmr/style.module.css @@ -0,0 +1,4 @@ +.app { + background: #f6f6f6; + color: #333; +}