Skip to content

Commit

Permalink
fix(gatsby): fix some css HMR edge cases (#29839) (#29865)
Browse files Browse the repository at this point in the history
* test(e2e-development-runtime): add test cases for various edge cases related to css HMR

* hackity hack

* Update packages/gatsby/src/utils/webpack/force-css-hmr-for-edge-cases.ts

Co-authored-by: Ward Peeters <ward@coding-tech.com>

* add some comments with explanation

Co-authored-by: Ward Peeters <ward@coding-tech.com>
(cherry picked from commit 52facaf)

Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com>
  • Loading branch information
GatsbyJS Bot and pieh committed Mar 1, 2021
1 parent e8a7e3b commit fdc1fe2
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 13 deletions.
172 changes: 161 additions & 11 deletions e2e-tests/development-runtime/cypress/integration/styling/plain-css.js
Expand Up @@ -7,29 +7,179 @@ after(() => {
})

describe(`styling: plain css`, () => {
beforeEach(() => {
it(`initial styling is correct`, () => {
cy.visit(`/styling/plain-css`).waitForRouteChange()
})

it(`initial styling is correct`, () => {
cy.getTestElement(`styled-element`).should(
`have.css`,
`color`,
`rgb(255, 0, 0)`
)
})

it(`updates on change`, () => {
cy.exec(
`npm run update -- --file src/pages/styling/plain-css.css --replacements "red:blue" --exact`
cy.getTestElement(`styled-element-that-is-not-styled-initially`).should(
`have.css`,
`color`,
`rgb(0, 0, 0)`
)

cy.waitForHmr()

cy.getTestElement(`styled-element`).should(
cy.getTestElement(`styled-element-by-not-visited-template`).should(
`have.css`,
`color`,
`rgb(0, 0, 255)`
`rgb(255, 0, 0)`
)

cy.getTestElement(
`styled-element-that-is-not-styled-initially-by-not-visited-template`
).should(`have.css`, `color`, `rgb(0, 0, 0)`)
})

describe(`changing styles/imports imported by visited template`, () => {
it(`updates on already imported css file change`, () => {
// we don't want to visit page for each test - we want to visit once and then test HMR
cy.window().then(win => {
cy.spy(win.console, `log`).as(`hmrConsoleLog`)
})

cy.exec(
`npm run update -- --file src/pages/styling/plain-css.css --replacements "red:blue" --exact`
)

cy.waitForHmr()

cy.getTestElement(`styled-element`).should(
`have.css`,
`color`,
`rgb(0, 0, 255)`
)
})

it(`importing new css file result in styles being applied`, () => {
// we don't want to visit page for each test - we want to visit once and then test HMR
cy.window().then(win => {
cy.spy(win.console, `log`).as(`hmrConsoleLog`)
})

cy.exec(
`npm run update -- --file src/pages/styling/plain-css.js --replacements "// UNCOMMENT-IN-TEST:/* IMPORT-TO-COMMENT-OUT-AGAIN */" --exact`
)

cy.waitForHmr()

cy.getTestElement(`styled-element-that-is-not-styled-initially`).should(
`have.css`,
`color`,
`rgb(255, 0, 0)`
)
})

it(`updating newly imported css file result in styles being applied`, () => {
// we don't want to visit page for each test - we want to visit once and then test HMR
cy.window().then(win => {
cy.spy(win.console, `log`).as(`hmrConsoleLog`)
})

cy.exec(
`npm run update -- --file src/pages/styling/plain-css-not-imported-initially.css --replacements "red:green" --exact`
)

cy.waitForHmr()

cy.getTestElement(`styled-element-that-is-not-styled-initially`).should(
`have.css`,
`color`,
`rgb(0, 128, 0)`
)
})

it(`removing css import results in styles being removed`, () => {
// we don't want to visit page for each test - we want to visit once and then test HMR
cy.window().then(win => {
cy.spy(win.console, `log`).as(`hmrConsoleLog`)
})

cy.exec(
`npm run update -- --file src/pages/styling/plain-css.js --replacements "/* IMPORT-TO-COMMENT-OUT-AGAIN */:// COMMENTED-AGAIN" --exact`
)

cy.waitForHmr()

cy.getTestElement(`styled-element-that-is-not-styled-initially`).should(
`have.css`,
`color`,
`rgb(0, 0, 0)`
)
})
})

describe(`changing styles/imports imported by NOT visited template`, () => {
it(`updates on already imported css file change by not visited template`, () => {
// we don't want to visit page for each test - we want to visit once and then test HMR
cy.window().then(win => {
cy.spy(win.console, `log`).as(`hmrConsoleLog`)
})

cy.exec(
`npm run update -- --file src/pages/styling/not-visited-plain-css.css --replacements "red:blue" --exact`
)

cy.waitForHmr()

cy.getTestElement(`styled-element-by-not-visited-template`).should(
`have.css`,
`color`,
`rgb(0, 0, 255)`
)
})

it(`importing new css file result in styles being applied`, () => {
// we don't want to visit page for each test - we want to visit once and then test HMR
cy.window().then(win => {
cy.spy(win.console, `log`).as(`hmrConsoleLog`)
})

cy.exec(
`npm run update -- --file src/pages/styling/not-visited-plain-css.js --replacements "// UNCOMMENT-IN-TEST:/* IMPORT-TO-COMMENT-OUT-AGAIN */" --exact`
)

cy.waitForHmr()

cy.getTestElement(
`styled-element-that-is-not-styled-initially-by-not-visited-template`
).should(`have.css`, `color`, `rgb(255, 0, 0)`)
})

it(`updating newly imported css file result in styles being applied`, () => {
// we don't want to visit page for each test - we want to visit once and then test HMR
cy.window().then(win => {
cy.spy(win.console, `log`).as(`hmrConsoleLog`)
})

cy.exec(
`npm run update -- --file src/pages/styling/not-visited-plain-css-not-imported-initially.css --replacements "red:green" --exact`
)

cy.waitForHmr()

cy.getTestElement(
`styled-element-that-is-not-styled-initially-by-not-visited-template`
).should(`have.css`, `color`, `rgb(0, 128, 0)`)
})

it(`removing css import results in styles being removed`, () => {
// we don't want to visit page for each test - we want to visit once and then test HMR
cy.window().then(win => {
cy.spy(win.console, `log`).as(`hmrConsoleLog`)
})

cy.exec(
`npm run update -- --file src/pages/styling/not-visited-plain-css.js --replacements "/* IMPORT-TO-COMMENT-OUT-AGAIN */:// COMMENTED-AGAIN" --exact`
)

cy.waitForHmr()

cy.getTestElement(
`styled-element-that-is-not-styled-initially-by-not-visited-template`
).should(`have.css`, `color`, `rgb(0, 0, 0)`)
})
})
})
@@ -0,0 +1,3 @@
.not-visited-plain-css-not-imported-initially {
color: red;
}
@@ -0,0 +1,3 @@
.not-visited-plain-css-test {
color: red;
}
@@ -0,0 +1,17 @@
import * as React from "react"

import "./not-visited-plain-css.css"
// UNCOMMENT-IN-TEST import "./not-visited-plain-css-not-imported-initially.css"

export default function PlainCss() {
return (
<>
<p>
This content doesn't matter - we never visit this page in tests - but
because we generate single global .css file, we want to test changing
css files imported by this module (and also adding new css imports).css
</p>
<p>css imported by this template is tested in `./plain-css.js` page</p>
</>
)
}
@@ -0,0 +1,3 @@
.plain-css-not-imported-initially {
color: red;
}
25 changes: 23 additions & 2 deletions e2e-tests/development-runtime/src/pages/styling/plain-css.js
@@ -1,11 +1,32 @@
import * as React from "react"

import "./plain-css.css"
// UNCOMMENT-IN-TEST import "./plain-css-not-imported-initially.css"

export default function PlainCss() {
return (
<div data-testid="styled-element" className="plain-css-test">
test
<div style={{ color: `black` }}>
<div data-testid="styled-element" className="plain-css-test">
test
</div>
<div
data-testid="styled-element-that-is-not-styled-initially"
className="plain-css-not-imported-initially"
>
test
</div>
<div
data-testid="styled-element-by-not-visited-template"
className="not-visited-plain-css-test"
>
test
</div>
<div
data-testid="styled-element-that-is-not-styled-initially-by-not-visited-template"
className="not-visited-plain-css-not-imported-initially "
>
test
</div>
</div>
)
}
2 changes: 2 additions & 0 deletions packages/gatsby/src/utils/webpack.config.js
Expand Up @@ -18,6 +18,7 @@ import { createWebpackUtils } from "./webpack-utils"
import { hasLocalEslint } from "./local-eslint-config-finder"
import { getAbsolutePathForVirtualModule } from "./gatsby-webpack-virtual-modules"
import { StaticQueryMapper } from "./webpack/static-query-mapper"
import { ForceCssHMRForEdgeCases } from "./webpack/force-css-hmr-for-edge-cases"
import { getBrowsersList } from "./browserslist"
import { builtinModules } from "module"

Expand Down Expand Up @@ -217,6 +218,7 @@ module.exports = async (
configPlugins = configPlugins
.concat([
plugins.fastRefresh({ modulesThatUseGatsby }),
new ForceCssHMRForEdgeCases(),
plugins.hotModuleReplacement(),
plugins.noEmitOnErrors(),
plugins.eslintGraphqlSchemaReload(),
Expand Down
106 changes: 106 additions & 0 deletions packages/gatsby/src/utils/webpack/force-css-hmr-for-edge-cases.ts
@@ -0,0 +1,106 @@
import { Compiler, Module } from "webpack"

/**
* This is total hack that is meant to handle:
* - https://github.com/webpack-contrib/mini-css-extract-plugin/issues/706
* - https://github.com/webpack-contrib/mini-css-extract-plugin/issues/708
* The way it works it is looking up what HotModuleReplacementPlugin checks internally
* and tricks it by checking up if any modules that uses mini-css-extract-plugin
* changed or was newly added and then modifying blank.css hash.
* blank.css is css module that is used by all pages and is there from the start
* so changing hash of that _should_ ensure that:
* - when new css is imported it will reload css
* - when css imported by not loaded (by runtime) page template changes it will reload css
*/
export class ForceCssHMRForEdgeCases {
private name: string
private originalBlankCssHash: string
private blankCssKey: string
private hackCounter = 0
private previouslySeenCss: Set<string> = new Set<string>()

constructor() {
this.name = `ForceCssHMRForEdgeCases`
}

apply(compiler: Compiler): void {
compiler.hooks.thisCompilation.tap(this.name, compilation => {
compilation.hooks.fullHash.tap(this.name, () => {
const chunkGraph = compilation.chunkGraph
const records = compilation.records

if (!records.chunkModuleHashes) {
return
}

const seenCssInThisCompilation = new Set<string>()
/**
* We will get list of css modules that are removed in this compilation
* by starting with list of css used in last compilation and removing
* all modules that are used in this one.
*/
const cssRemovedInThisCompilation = this.previouslySeenCss

let newOrUpdatedCss = false

for (const chunk of compilation.chunks) {
const getModuleHash = (module: Module): string => {
if (compilation.codeGenerationResults.has(module, chunk.runtime)) {
return compilation.codeGenerationResults.getHash(
module,
chunk.runtime
)
} else {
return chunkGraph.getModuleHash(module, chunk.runtime)
}
}

const modules = chunkGraph.getChunkModulesIterable(chunk)

if (modules !== undefined) {
for (const module of modules) {
const key = `${chunk.id}|${module.identifier()}`

if (
!this.originalBlankCssHash &&
module.rawRequest === `./blank.css`
) {
this.blankCssKey = key
this.originalBlankCssHash =
records.chunkModuleHashes[this.blankCssKey]
}

const isUsingMiniCssExtract = module.loaders?.find(loader =>
loader?.loader?.includes(`mini-css-extract-plugin`)
)

if (isUsingMiniCssExtract) {
seenCssInThisCompilation.add(key)
cssRemovedInThisCompilation.delete(key)

const hash = getModuleHash(module)
if (records.chunkModuleHashes[key] !== hash) {
newOrUpdatedCss = true
}
}
}
}
}

// If css file was edited or new css import was added (`newOrUpdatedCss`)
// or if css import was removed (`cssRemovedInThisCompilation.size > 0`)
// trick Webpack's HMR into thinking `blank.css` file changed.
if (
(newOrUpdatedCss || cssRemovedInThisCompilation.size > 0) &&
this.originalBlankCssHash &&
this.blankCssKey
) {
records.chunkModuleHashes[this.blankCssKey] =
this.originalBlankCssHash + String(this.hackCounter++)
}

this.previouslySeenCss = seenCssInThisCompilation
})
})
}
}

0 comments on commit fdc1fe2

Please sign in to comment.