From be6dea8b30bf07be327b6d397401a19d247e4125 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 28 Feb 2021 17:18:35 +0100 Subject: [PATCH 01/11] fix duplicate css crashing --- packages/wmr/demo/public/a.css | 1 + packages/wmr/demo/public/b.css | 1 + packages/wmr/demo/public/c.css | 1 + packages/wmr/demo/public/index.tsx | 3 +++ packages/wmr/src/plugins/optimize-graph-plugin.js | 5 ++++- 5 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 packages/wmr/demo/public/a.css create mode 100644 packages/wmr/demo/public/b.css create mode 100644 packages/wmr/demo/public/c.css diff --git a/packages/wmr/demo/public/a.css b/packages/wmr/demo/public/a.css new file mode 100644 index 000000000..12dca5a01 --- /dev/null +++ b/packages/wmr/demo/public/a.css @@ -0,0 +1 @@ +a { text-decoration: underline; } diff --git a/packages/wmr/demo/public/b.css b/packages/wmr/demo/public/b.css new file mode 100644 index 000000000..addbe6571 --- /dev/null +++ b/packages/wmr/demo/public/b.css @@ -0,0 +1 @@ +a { text-decoration: none; } diff --git a/packages/wmr/demo/public/c.css b/packages/wmr/demo/public/c.css new file mode 100644 index 000000000..12dca5a01 --- /dev/null +++ b/packages/wmr/demo/public/c.css @@ -0,0 +1 @@ +a { text-decoration: underline; } diff --git a/packages/wmr/demo/public/index.tsx b/packages/wmr/demo/public/index.tsx index 9b50a00b2..d6e2fa87e 100644 --- a/packages/wmr/demo/public/index.tsx +++ b/packages/wmr/demo/public/index.tsx @@ -5,6 +5,9 @@ import Home from './pages/home.js'; // import About from './pages/about/index.js'; import NotFound from './pages/_404.js'; import Header from './header.tsx'; +import './a.css'; +import './b.css'; +import './c.css'; // import './style.css'; const About = lazy(() => import('./pages/about/index.js')); diff --git a/packages/wmr/src/plugins/optimize-graph-plugin.js b/packages/wmr/src/plugins/optimize-graph-plugin.js index 23e88a1e6..60a507773 100644 --- a/packages/wmr/src/plugins/optimize-graph-plugin.js +++ b/packages/wmr/src/plugins/optimize-graph-plugin.js @@ -223,8 +223,11 @@ function mergeAdjacentCss(graph) { } const base = /** @type {Asset} */ (graph.bundle[toMerge[0]]); + const visited = new Set(); for (let i = 1; i < toMerge.length; i++) { const f = toMerge[i]; + if (visited.has(f)) continue; + visited.add(f); const asset = /** @type {Asset} */ (graph.bundle[f]); base.source += '\n' + asset.source; chunk.referencedFiles.splice(chunk.referencedFiles.indexOf(f), 1); @@ -262,7 +265,7 @@ function hoistEntryCss(graph) { for (let i = 0; i < chunk.referencedFiles.length; i++) { const f = chunk.referencedFiles[i]; - if (!isCssFilename(f)) continue; + if (!isCssFilename(f) || !graph.bundle[f]) continue; if (cssAsset) { if (DEBUG) console.log(`Hoisting CSS "${f}" imported by ${id} into parent HTML import "${cssImport}".`); // @TODO: this needs to update the hash of the chunk into which CSS got merged. From 05f279e4f2c91c9b2186c956a5aa55f5f3a44afc Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 28 Feb 2021 20:35:12 +0100 Subject: [PATCH 02/11] tweak implementation --- .../wmr/src/plugins/optimize-graph-plugin.js | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/wmr/src/plugins/optimize-graph-plugin.js b/packages/wmr/src/plugins/optimize-graph-plugin.js index 60a507773..04d81734c 100644 --- a/packages/wmr/src/plugins/optimize-graph-plugin.js +++ b/packages/wmr/src/plugins/optimize-graph-plugin.js @@ -26,9 +26,15 @@ export default function optimizeGraphPlugin({ publicPath = '', cssMinSize = 1000 name: 'optimize-graph', async generateBundle(_, bundle) { const graph = new ChunkGraph(bundle, { publicPath }); - mergeAdjacentCss(graph); - hoistCascadedCss(graph, { cssMinSize }); - hoistEntryCss(graph); + let toRemove = []; + toRemove = [...toRemove, ...mergeAdjacentCss(graph)]; + toRemove = [...toRemove, ...hoistCascadedCss(graph, { cssMinSize })]; + toRemove = [...toRemove, ...hoistEntryCss(graph)]; + + toRemove.forEach(f => { + delete graph.bundle[f]; + }); + hoistTransitiveImports(graph); } }; @@ -205,6 +211,7 @@ function toImport(publicPath, filename) { * @param {ChunkGraph} graph */ function mergeAdjacentCss(graph) { + const toRemove = new Set(); for (const fileName in graph.bundle) { const chunk = graph.bundle[fileName]; if (chunk.type !== 'chunk') continue; @@ -223,15 +230,12 @@ function mergeAdjacentCss(graph) { } const base = /** @type {Asset} */ (graph.bundle[toMerge[0]]); - const visited = new Set(); for (let i = 1; i < toMerge.length; i++) { const f = toMerge[i]; - if (visited.has(f)) continue; - visited.add(f); const asset = /** @type {Asset} */ (graph.bundle[f]); base.source += '\n' + asset.source; chunk.referencedFiles.splice(chunk.referencedFiles.indexOf(f), 1); - delete graph.bundle[f]; + toRemove.add(f); } // Remove all but the first style("url") "import": @@ -243,6 +247,8 @@ function mergeAdjacentCss(graph) { return isFirst && toMerge[0]; }); } + + return toRemove; } /** @@ -250,6 +256,7 @@ function mergeAdjacentCss(graph) { * @param {ChunkGraph} graph */ function hoistEntryCss(graph) { + const toRemove = new Set(); for (const fileName in graph.bundle) { /** @type {ExtendedAsset | Chunk} */ const asset = graph.bundle[fileName]; @@ -265,13 +272,13 @@ function hoistEntryCss(graph) { for (let i = 0; i < chunk.referencedFiles.length; i++) { const f = chunk.referencedFiles[i]; - if (!isCssFilename(f) || !graph.bundle[f]) continue; + if (!isCssFilename(f)) continue; if (cssAsset) { if (DEBUG) console.log(`Hoisting CSS "${f}" imported by ${id} into parent HTML import "${cssImport}".`); // @TODO: this needs to update the hash of the chunk into which CSS got merged. cssAsset.source += '\n' + getAssetSource(graph.bundle[f]); chunk.referencedFiles[i] = cssImport; - delete graph.bundle[f]; + toRemove.add(f); graph.replaceCssImport(chunk, f, false); const chunkInfo = graph.assetToChunkMap.get(f); if (chunkInfo) chunkInfo.mergedInto = cssImport; @@ -293,6 +300,8 @@ function hoistEntryCss(graph) { } } } + + return toRemove; } /** @@ -300,6 +309,7 @@ function hoistEntryCss(graph) { * @param {ChunkGraph} graph */ function hoistCascadedCss(graph, { cssMinSize }) { + const toRemove = new Set(); for (const fileName in graph.bundle) { const asset = graph.bundle[fileName]; if (asset.type !== 'asset' || !isCssFilename(asset.fileName)) continue; @@ -334,7 +344,7 @@ function hoistCascadedCss(graph, { cssMinSize }) { const parentAsset = /** @type {Asset} */ (graph.bundle[ownCss]); parentAsset.source += '\n' + asset.source; graph.replaceCssImport(ownerChunk, fileName, ownCss); - delete graph.bundle[fileName]; + toRemove.add(fileName); chunkInfo.mergedInto = ownCss; // chunkInfo.chunks = [parentFileName]; chunkInfo.chunks.unshift(parentFileName); @@ -356,6 +366,8 @@ function hoistCascadedCss(graph, { cssMinSize }) { break; } } + + return toRemove; } /** From 2e14e91d0c7cb17dd68d7bdcffe7b88cfa8f1f39 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 28 Feb 2021 20:40:58 +0100 Subject: [PATCH 03/11] add changeset and revert example --- .changeset/orange-vans-trade.md | 5 +++++ packages/wmr/demo/public/a.css | 1 - packages/wmr/demo/public/b.css | 1 - packages/wmr/demo/public/c.css | 1 - packages/wmr/demo/public/index.tsx | 3 --- 5 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 .changeset/orange-vans-trade.md delete mode 100644 packages/wmr/demo/public/a.css delete mode 100644 packages/wmr/demo/public/b.css delete mode 100644 packages/wmr/demo/public/c.css diff --git a/.changeset/orange-vans-trade.md b/.changeset/orange-vans-trade.md new file mode 100644 index 000000000..a69c53c3f --- /dev/null +++ b/.changeset/orange-vans-trade.md @@ -0,0 +1,5 @@ +--- +'wmr': patch +--- + +Fix case where deduped css would crash the bundle diff --git a/packages/wmr/demo/public/a.css b/packages/wmr/demo/public/a.css deleted file mode 100644 index 12dca5a01..000000000 --- a/packages/wmr/demo/public/a.css +++ /dev/null @@ -1 +0,0 @@ -a { text-decoration: underline; } diff --git a/packages/wmr/demo/public/b.css b/packages/wmr/demo/public/b.css deleted file mode 100644 index addbe6571..000000000 --- a/packages/wmr/demo/public/b.css +++ /dev/null @@ -1 +0,0 @@ -a { text-decoration: none; } diff --git a/packages/wmr/demo/public/c.css b/packages/wmr/demo/public/c.css deleted file mode 100644 index 12dca5a01..000000000 --- a/packages/wmr/demo/public/c.css +++ /dev/null @@ -1 +0,0 @@ -a { text-decoration: underline; } diff --git a/packages/wmr/demo/public/index.tsx b/packages/wmr/demo/public/index.tsx index d6e2fa87e..9b50a00b2 100644 --- a/packages/wmr/demo/public/index.tsx +++ b/packages/wmr/demo/public/index.tsx @@ -5,9 +5,6 @@ import Home from './pages/home.js'; // import About from './pages/about/index.js'; import NotFound from './pages/_404.js'; import Header from './header.tsx'; -import './a.css'; -import './b.css'; -import './c.css'; // import './style.css'; const About = lazy(() => import('./pages/about/index.js')); From 6b8658c635f779788ecd602bdccb81519b6aca72 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 28 Feb 2021 16:32:40 -0500 Subject: [PATCH 04/11] Add test for graph optimization of deduplicated CSS --- .../wmr/test/fixtures/css-duplicates/a.css | 1 + .../wmr/test/fixtures/css-duplicates/b.css | 1 + .../wmr/test/fixtures/css-duplicates/c.css | 1 + .../test/fixtures/css-duplicates/index.html | 1 + .../wmr/test/fixtures/css-duplicates/index.js | 4 +++ .../wmr/test/fixtures/css-duplicates/lazy.js | 1 + packages/wmr/test/production.test.js | 31 +++++++++++++++++++ 7 files changed, 40 insertions(+) create mode 100644 packages/wmr/test/fixtures/css-duplicates/a.css create mode 100644 packages/wmr/test/fixtures/css-duplicates/b.css create mode 100644 packages/wmr/test/fixtures/css-duplicates/c.css create mode 100644 packages/wmr/test/fixtures/css-duplicates/index.html create mode 100644 packages/wmr/test/fixtures/css-duplicates/index.js create mode 100644 packages/wmr/test/fixtures/css-duplicates/lazy.js diff --git a/packages/wmr/test/fixtures/css-duplicates/a.css b/packages/wmr/test/fixtures/css-duplicates/a.css new file mode 100644 index 000000000..5d6ab09e9 --- /dev/null +++ b/packages/wmr/test/fixtures/css-duplicates/a.css @@ -0,0 +1 @@ +a { color: #f00; } diff --git a/packages/wmr/test/fixtures/css-duplicates/b.css b/packages/wmr/test/fixtures/css-duplicates/b.css new file mode 100644 index 000000000..5d6ab09e9 --- /dev/null +++ b/packages/wmr/test/fixtures/css-duplicates/b.css @@ -0,0 +1 @@ +a { color: #f00; } diff --git a/packages/wmr/test/fixtures/css-duplicates/c.css b/packages/wmr/test/fixtures/css-duplicates/c.css new file mode 100644 index 000000000..269adfa22 --- /dev/null +++ b/packages/wmr/test/fixtures/css-duplicates/c.css @@ -0,0 +1 @@ +a { color: #00f; text-decoration: underline; } diff --git a/packages/wmr/test/fixtures/css-duplicates/index.html b/packages/wmr/test/fixtures/css-duplicates/index.html new file mode 100644 index 000000000..f0dbd83b4 --- /dev/null +++ b/packages/wmr/test/fixtures/css-duplicates/index.html @@ -0,0 +1 @@ + diff --git a/packages/wmr/test/fixtures/css-duplicates/index.js b/packages/wmr/test/fixtures/css-duplicates/index.js new file mode 100644 index 000000000..5fa1be0ce --- /dev/null +++ b/packages/wmr/test/fixtures/css-duplicates/index.js @@ -0,0 +1,4 @@ +import './a.css'; +import './b.css'; +import './c.css'; +import('./lazy.js'); diff --git a/packages/wmr/test/fixtures/css-duplicates/lazy.js b/packages/wmr/test/fixtures/css-duplicates/lazy.js new file mode 100644 index 000000000..d6db9c31a --- /dev/null +++ b/packages/wmr/test/fixtures/css-duplicates/lazy.js @@ -0,0 +1 @@ +import './b.css'; diff --git a/packages/wmr/test/production.test.js b/packages/wmr/test/production.test.js index 54d0942c8..baead1d1d 100644 --- a/packages/wmr/test/production.test.js +++ b/packages/wmr/test/production.test.js @@ -183,6 +183,37 @@ describe('production', () => { expect.stringMatching(/^\/assets\/style\.\w+\.css$/) ]); }); + + it('should merge duplicate CSS imports', async () => { + await loadFixture('css-duplicates', env); + instance = await runWmr(env.tmp.path, 'build'); + const code = await instance.done; + console.info(instance.output.join('\n')); + expect(code).toBe(0); + + const readdir = async f => (await fs.readdir(path.join(env.tmp.path, f))).filter(f => f[0] !== '.'); + const readfile = async f => await fs.readFile(path.join(env.tmp.path, f), 'utf-8'); + + const dist = await readdir('dist'); + + const chunks = await readdir('dist/chunks'); + expect(chunks).toEqual([expect.stringMatching(/^lazy\.\w+\.js$/)]); + + const assets = await readdir('dist/assets'); + // TODO: Rollup seems to randomly pick which asset ID to use for duplicated assets: + expect(assets).toEqual([expect.stringMatching(/^[ab]\.\w+\.css$/)]); + + const css = await readfile('dist/assets/' + assets[0]); + + // ensure all the CSS properties got merged into a single rule: + expect(css).toMatch(/a{[^{}]+}/); + + const properties = css.slice(2, -1).split(';').sort(); + expect(properties).toEqual(['color:#00f', 'color:red', 'text-decoration:underline']); + + const index = await readfile('dist/' + dist.find(f => f.match(/^index\.\w+\.js$/))); + expect(index).toContain(`("/assets/${assets[0]}")`); + }); }); describe('config.publicPath', () => { From f7f82b9a70554d638d0dd83ec5fb1628b16f73b7 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 28 Feb 2021 16:35:55 -0500 Subject: [PATCH 05/11] Move deferred bundle deletions onto the Graph instance --- .../wmr/src/plugins/optimize-graph-plugin.js | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/wmr/src/plugins/optimize-graph-plugin.js b/packages/wmr/src/plugins/optimize-graph-plugin.js index 04d81734c..d33210909 100644 --- a/packages/wmr/src/plugins/optimize-graph-plugin.js +++ b/packages/wmr/src/plugins/optimize-graph-plugin.js @@ -26,15 +26,10 @@ export default function optimizeGraphPlugin({ publicPath = '', cssMinSize = 1000 name: 'optimize-graph', async generateBundle(_, bundle) { const graph = new ChunkGraph(bundle, { publicPath }); - let toRemove = []; - toRemove = [...toRemove, ...mergeAdjacentCss(graph)]; - toRemove = [...toRemove, ...hoistCascadedCss(graph, { cssMinSize })]; - toRemove = [...toRemove, ...hoistEntryCss(graph)]; - - toRemove.forEach(f => { - delete graph.bundle[f]; - }); - + mergeAdjacentCss(graph); + hoistCascadedCss(graph, { cssMinSize }); + hoistEntryCss(graph); + graph.commit(); hoistTransitiveImports(graph); } }; @@ -48,6 +43,7 @@ export default function optimizeGraphPlugin({ publicPath = '', cssMinSize = 1000 class ChunkGraph { constructor(bundle, { publicPath }) { this.bundle = bundle; + this.bundleRemoved = new Set(); this.assetToChunkMap = constructAssetToChunkMap(bundle); this.entries = this.findEntryChunks(); this.meta = new Map(); @@ -55,6 +51,12 @@ class ChunkGraph { // this.entryAssets = this.findEntryAssets(); } + commit() { + this.bundleRemoved.forEach(f => { + delete this.bundle[f]; + }); + } + /** * Get a mutable object for storing metadata about a chunk/asset. * @param {string} fileName @@ -211,7 +213,6 @@ function toImport(publicPath, filename) { * @param {ChunkGraph} graph */ function mergeAdjacentCss(graph) { - const toRemove = new Set(); for (const fileName in graph.bundle) { const chunk = graph.bundle[fileName]; if (chunk.type !== 'chunk') continue; @@ -235,7 +236,7 @@ function mergeAdjacentCss(graph) { const asset = /** @type {Asset} */ (graph.bundle[f]); base.source += '\n' + asset.source; chunk.referencedFiles.splice(chunk.referencedFiles.indexOf(f), 1); - toRemove.add(f); + graph.bundleRemoved.add(f); } // Remove all but the first style("url") "import": @@ -247,8 +248,6 @@ function mergeAdjacentCss(graph) { return isFirst && toMerge[0]; }); } - - return toRemove; } /** @@ -256,7 +255,6 @@ function mergeAdjacentCss(graph) { * @param {ChunkGraph} graph */ function hoistEntryCss(graph) { - const toRemove = new Set(); for (const fileName in graph.bundle) { /** @type {ExtendedAsset | Chunk} */ const asset = graph.bundle[fileName]; @@ -278,7 +276,7 @@ function hoistEntryCss(graph) { // @TODO: this needs to update the hash of the chunk into which CSS got merged. cssAsset.source += '\n' + getAssetSource(graph.bundle[f]); chunk.referencedFiles[i] = cssImport; - toRemove.add(f); + graph.bundleRemoved.add(f); graph.replaceCssImport(chunk, f, false); const chunkInfo = graph.assetToChunkMap.get(f); if (chunkInfo) chunkInfo.mergedInto = cssImport; @@ -300,8 +298,6 @@ function hoistEntryCss(graph) { } } } - - return toRemove; } /** @@ -309,7 +305,6 @@ function hoistEntryCss(graph) { * @param {ChunkGraph} graph */ function hoistCascadedCss(graph, { cssMinSize }) { - const toRemove = new Set(); for (const fileName in graph.bundle) { const asset = graph.bundle[fileName]; if (asset.type !== 'asset' || !isCssFilename(asset.fileName)) continue; @@ -344,7 +339,7 @@ function hoistCascadedCss(graph, { cssMinSize }) { const parentAsset = /** @type {Asset} */ (graph.bundle[ownCss]); parentAsset.source += '\n' + asset.source; graph.replaceCssImport(ownerChunk, fileName, ownCss); - toRemove.add(fileName); + graph.bundleRemoved.add(fileName); chunkInfo.mergedInto = ownCss; // chunkInfo.chunks = [parentFileName]; chunkInfo.chunks.unshift(parentFileName); @@ -366,8 +361,6 @@ function hoistCascadedCss(graph, { cssMinSize }) { break; } } - - return toRemove; } /** From fc63068a6f8573a81a78c17bff454414b5dabad6 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 28 Feb 2021 16:37:04 -0500 Subject: [PATCH 06/11] Turns out we can just treat adjacent CSS as unique, since Rollup pre-merges identical referencedAssets. --- .../wmr/src/plugins/optimize-graph-plugin.js | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/wmr/src/plugins/optimize-graph-plugin.js b/packages/wmr/src/plugins/optimize-graph-plugin.js index d33210909..ce6405b6e 100644 --- a/packages/wmr/src/plugins/optimize-graph-plugin.js +++ b/packages/wmr/src/plugins/optimize-graph-plugin.js @@ -217,11 +217,13 @@ function mergeAdjacentCss(graph) { const chunk = graph.bundle[fileName]; if (chunk.type !== 'chunk') continue; - const toMerge = chunk.referencedFiles.filter(assetFileName => { - if (!isCssFilename(assetFileName)) return false; - const chunkInfo = graph.assetToChunkMap.get(assetFileName); - return chunkInfo && chunkInfo.chunks.length === 1; - }); + const toMerge = unique( + chunk.referencedFiles.filter(assetFileName => { + if (!isCssFilename(assetFileName)) return false; + const chunkInfo = graph.assetToChunkMap.get(assetFileName); + return chunkInfo && chunkInfo.chunks.length === 1; + }) + ); if (toMerge.length < 2) continue; @@ -236,7 +238,7 @@ function mergeAdjacentCss(graph) { const asset = /** @type {Asset} */ (graph.bundle[f]); base.source += '\n' + asset.source; chunk.referencedFiles.splice(chunk.referencedFiles.indexOf(f), 1); - graph.bundleRemoved.add(f); + delete graph.bundle[f]; } // Remove all but the first style("url") "import": @@ -276,7 +278,7 @@ function hoistEntryCss(graph) { // @TODO: this needs to update the hash of the chunk into which CSS got merged. cssAsset.source += '\n' + getAssetSource(graph.bundle[f]); chunk.referencedFiles[i] = cssImport; - graph.bundleRemoved.add(f); + delete graph.bundle[f]; graph.replaceCssImport(chunk, f, false); const chunkInfo = graph.assetToChunkMap.get(f); if (chunkInfo) chunkInfo.mergedInto = cssImport; @@ -339,7 +341,7 @@ function hoistCascadedCss(graph, { cssMinSize }) { const parentAsset = /** @type {Asset} */ (graph.bundle[ownCss]); parentAsset.source += '\n' + asset.source; graph.replaceCssImport(ownerChunk, fileName, ownCss); - graph.bundleRemoved.add(fileName); + delete graph.bundle[fileName]; chunkInfo.mergedInto = ownCss; // chunkInfo.chunks = [parentFileName]; chunkInfo.chunks.unshift(parentFileName); @@ -502,3 +504,6 @@ function getAssetSource(asset) { } return code; } + +/** @param {Array} arr @returns {Array} */ +const unique = arr => Array.from(new Set(arr)); From 4e6aed1425e84ba312775a130af99e599b775444 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 14 Mar 2021 16:15:34 -0400 Subject: [PATCH 07/11] Rewrite tests to avoid relying on timers --- packages/wmr/test/fixtures.test.js | 160 +++++++++++++++-------------- 1 file changed, 83 insertions(+), 77 deletions(-) diff --git a/packages/wmr/test/fixtures.test.js b/packages/wmr/test/fixtures.test.js index faed0ee78..fc08b4bb5 100644 --- a/packages/wmr/test/fixtures.test.js +++ b/packages/wmr/test/fixtures.test.js @@ -203,113 +203,119 @@ describe('fixtures', () => { 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); + async function waitForText(element, text, timeoutAfter = 5000) { + const start = Date.now(); + let t; + while (Date.now() - start < timeoutAfter) { + t = await element.evaluate(el => el.textContent); + if (t === text) break; + await timeout(100); + } + return t; + } - text = home ? await home.evaluate(el => el.textContent) : null; - expect(text).toEqual('Away'); - }); + async function waitForValue(page, fn, value) { + return (await page.waitForFunction(`value => (${fn})() === value`, { timeout: 1000 }, value)) && value; + } - it('should hot reload for a newly created file', async () => { - await loadFixture('hmr', env); - instance = await runWmrFast(env.tmp.path); - await getOutput(env, instance); + describe('JavaScript', () => { + it('should hot reload the child-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'); + expect(await waitForText(home, 'Home', 1000)).toEqual('Home'); - 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 updateFile(env.tmp.path, 'home.js', content => { - const newContent = `import Child from './child.js';\n\n${content}`; - return newContent.replace('

Home

', ''); + expect(await waitForText(home, 'Away', 5000)).toEqual('Away'); }); - await timeout(1000); + 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 child = await env.page.$('.child'); - text = child ? await child.evaluate(el => el.textContent) : null; - expect(text).toEqual('child'); - }); + const home = await env.page.waitForSelector('.home', { timeout: 1000 }); + expect(home.evaluate(el => el.textContent)).toEqual('Home'); - 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); + const compPath = path.join(env.tmp.path, 'child.js'); + await fs.writeFile(compPath, `export default function Child() {return

child

}`); - expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); + await timeout(1000); - await updateFile(env.tmp.path, 'index.css', content => content.replace('color: #333;', 'color: #000;')); + expect(await home.evaluate(el => el.textContent)).toEqual('Home'); - await timeout(1000); + await updateFile(env.tmp.path, 'home.js', content => { + const newContent = `import Child from './child.js';\n\n${content}`; + return newContent.replace('

Home

', ''); + }); - expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 0)'); - }); + const child = await env.page.waitForSelector('.child', { timeout: 1000 }); + expect(await child.evaluate(el => el.textContent)).toEqual('child'); - it('should hot reload a module css-file', async () => { - await loadFixture('hmr', env); - instance = await runWmrFast(env.tmp.path); - await getOutput(env, instance); + // Ensure we're still on the page and haven't navigated away: + expect(await home.evaluate(el => el.textContent)).toEqual('Home'); + }); + }); - expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); + describe('CSS', () => { + 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); - await updateFile(env.tmp.path, 'style.module.css', content => content.replace('color: #333;', 'color: #000;')); + expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); - await timeout(1000); + await updateFile(env.tmp.path, 'index.css', content => content.replace('color: #333;', 'color: #000;')); - expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 0)'); - }); - }); + expect(await waitForValue(page, () => getComputedStyle(document.body), 'rgb(0, 0, 0)')).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)); - } + it('should hot reload a module css-file', async () => { + await loadFixture('hmr', env); + instance = await runWmrFast(env.tmp.path); + await getOutput(env, instance); - const timeout = n => new Promise(r => setTimeout(r, n)); + expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); - 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); + await updateFile(env.tmp.path, 'style.module.css', content => content.replace('color: #333;', 'color: #000;')); - expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); + expect(await waitForValue(page, () => getComputedStyle(document.querySelector('main')), 'rgb(0, 0, 0)')).toBe( + 'rgb(0, 0, 0)' + ); + }); + }); - await updateFile(env.tmp.path, 'index.scss', content => content.replace('color: #333;', 'color: #000;')); + describe('SCSS', () => { + 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); - await timeout(1000); + expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); - expect(await page.$eval('body', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 0)'); - }); + await updateFile(env.tmp.path, 'index.scss', content => content.replace('color: #333;', 'color: #000;')); - 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 waitForValue(page, () => getComputedStyle(document.body), 'rgb(0, 0, 0)')).toBe('rgb(0, 0, 0)'); + }); - expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); + 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); - await updateFile(env.tmp.path, 'home.scss', content => content.replace('color: #333;', 'color: #000;')); + expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(51, 51, 51)'); - await timeout(1000); + await updateFile(env.tmp.path, 'home.scss', content => content.replace('color: #333;', 'color: #000;')); - expect(await page.$eval('main', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 0)'); + expect(await waitForValue(page, () => getComputedStyle(document.querySelector('main')), 'rgb(0, 0, 0)')).toBe( + 'rgb(0, 0, 0)' + ); + }); }); }); From 7eabd0f5aa8cbaac60add07b616eb2316cd7607a Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 14 Mar 2021 16:15:54 -0400 Subject: [PATCH 08/11] Fix flakey prod test --- packages/wmr/test/production.test.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/wmr/test/production.test.js b/packages/wmr/test/production.test.js index baead1d1d..007288f14 100644 --- a/packages/wmr/test/production.test.js +++ b/packages/wmr/test/production.test.js @@ -316,13 +316,11 @@ describe('production', () => { waitUntil: ['networkidle0', 'load'] }); - expect(logs).toEqual([ - `hello from index.js`, - `hello from page one`, - `hello from page two`, - `loaded pages`, - `page one,page two` - ]); + expect(logs).toContain(`hello from index.js`); + expect(logs).toContain(`hello from page one`); + expect(logs).toContain(`hello from page two`); + expect(logs).toContain(`loaded pages`); + expect(logs).toContain(`page one,page two`); }); }); }); From de0e79a75e294ccdb6b02eb44cb5d67029836797 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 14 Mar 2021 16:16:33 -0400 Subject: [PATCH 09/11] Update cssnano-lite to use identical plugin ordering to cssnano. --- packages/wmr/src/lib/cssnano-lite.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/wmr/src/lib/cssnano-lite.js b/packages/wmr/src/lib/cssnano-lite.js index 4f7ffcc1c..5a0bd9291 100644 --- a/packages/wmr/src/lib/cssnano-lite.js +++ b/packages/wmr/src/lib/cssnano-lite.js @@ -3,6 +3,7 @@ * This is essentially the same plugins, but without SVGO. */ import postcss from 'postcss'; +import cssDeclarationSorter from 'css-declaration-sorter'; import discardComments from 'postcss-discard-comments'; import postcssReduceInitial from 'postcss-reduce-initial'; import postcssReduceTransforms from 'postcss-reduce-transforms'; @@ -50,27 +51,28 @@ const plugins = [ discardComments, postcssReduceInitial, postcssReduceTransforms, - postcssConvertValues, - postcssCalc, + postcssNormalizeDisplayValues, postcssColormin, + postcssNormalizeTimingFunctions, + postcssCalc, + postcssConvertValues, postcssOrderedValues, postcssMinifySelectors, postcssMinifyParams, + postcssDiscardOverridden, + postcssNormalizeString, + postcssNormalizeUnicode, postcssMinifyFontValues, postcssNormalizeUrl, + postcssNormalizeRepeatStyle, + postcssNormalizePositions, + postcssNormalizeWhitespace, postcssMergeLonghand, postcssDiscardDuplicates, - postcssDiscardOverridden, - postcssNormalizeRepeatStyle, postcssMergeRules, postcssDiscardEmpty, postcssUniqueSelectors, - postcssNormalizeString, - postcssNormalizePositions, - postcssNormalizeWhitespace, - postcssNormalizeUnicode, - postcssNormalizeDisplayValues, - postcssNormalizeTimingFunctions, + cssDeclarationSorter, rawCache ].map(fn => (fn && fn.default) || fn); From 1ae5d6514f6ce08484cd24b7f16830c353f4899a Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 14 Mar 2021 16:17:32 -0400 Subject: [PATCH 10/11] tsignore --- packages/wmr/src/lib/cssnano-lite.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wmr/src/lib/cssnano-lite.js b/packages/wmr/src/lib/cssnano-lite.js index 5a0bd9291..0d7b3aebb 100644 --- a/packages/wmr/src/lib/cssnano-lite.js +++ b/packages/wmr/src/lib/cssnano-lite.js @@ -31,6 +31,7 @@ import postcssNormalizeTimingFunctions from 'postcss-normalize-timing-functions' const rawCache = postcss.plugin('cssnano-util-raw-cache', () => { return (css, result) => { + // @ts-ignore-next result.root.rawCache = { colon: ':', indent: '', From 1ba597e7c1d3490a91ba0cf40a7df009049a4374 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 14 Mar 2021 16:29:10 -0400 Subject: [PATCH 11/11] tsignore instead of Node 13 syntax --- packages/wmr/test/fixtures.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/wmr/test/fixtures.test.js b/packages/wmr/test/fixtures.test.js index 1cb7939f4..c3c9c31d1 100644 --- a/packages/wmr/test/fixtures.test.js +++ b/packages/wmr/test/fixtures.test.js @@ -229,7 +229,8 @@ describe('fixtures', () => { expect(text).toEqual('0'); let increment = await env.page.$('.increment'); - await increment?.click(); + // @ts-ignore-next + await increment.click(); text = count ? await count.evaluate(el => el.textContent) : null; expect(text).toEqual('1'); @@ -240,7 +241,8 @@ describe('fixtures', () => { await timeout(2000); increment = await env.page.$('.increment'); - await increment?.click(); + // @ts-ignore-next + await increment.click(); text = count ? await count.evaluate(el => el.textContent) : null; expect(text).toEqual('3'); });