diff --git a/integration-tests/ssr/__mocks__/styleMock.js b/integration-tests/ssr/__mocks__/styleMock.js new file mode 100644 index 0000000000000..4ba52ba2c8df6 --- /dev/null +++ b/integration-tests/ssr/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap b/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap index 716569a481059..fb84baa33e754 100644 --- a/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap +++ b/integration-tests/ssr/__tests__/__snapshots__/ssr.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SSR is run for a page when it is requested 1`] = `"
Hello world
"`; +exports[`SSR is run for a page when it is requested 1`] = `"

Hello world

"`; exports[`SSR it generates an error page correctly 1`] = ` " diff --git a/integration-tests/ssr/__tests__/ssr.js b/integration-tests/ssr/__tests__/ssr.js index c09a8923a5bcf..25b0cccd0d95e 100644 --- a/integration-tests/ssr/__tests__/ssr.js +++ b/integration-tests/ssr/__tests__/ssr.js @@ -53,5 +53,5 @@ describe(`SSR`, () => { }, 400) }, 400) }) - }) + }, 15000) }) diff --git a/integration-tests/ssr/gatsby-browser.js b/integration-tests/ssr/gatsby-browser.js new file mode 100644 index 0000000000000..d30aa3ea07cd5 --- /dev/null +++ b/integration-tests/ssr/gatsby-browser.js @@ -0,0 +1 @@ +import "./sample.css" diff --git a/integration-tests/ssr/gatsby-config.js b/integration-tests/ssr/gatsby-config.js index cc785a9507538..7227b35bf4e61 100644 --- a/integration-tests/ssr/gatsby-config.js +++ b/integration-tests/ssr/gatsby-config.js @@ -6,5 +6,5 @@ module.exports = { github: `sidharthachatterjee`, moreInfo: `Sid is amazing`, }, - plugins: [], + plugins: ["gatsby-plugin-postcss"], } diff --git a/integration-tests/ssr/jest.config.js b/integration-tests/ssr/jest.config.js index 4e5a78b25d7bf..4abafe97fa92b 100644 --- a/integration-tests/ssr/jest.config.js +++ b/integration-tests/ssr/jest.config.js @@ -1,3 +1,14 @@ module.exports = { - testPathIgnorePatterns: [`/node_modules/`, `__tests__/fixtures`, `.cache`], + testPathIgnorePatterns: [ + `/node_modules/`, + `__tests__/fixtures`, + `.cache`, + `src/test`, + ], + transform: { + "^.+\\.[jt]sx?$": `../../jest-transformer.js`, + }, + moduleNameMapper: { + "\\.(css)$": `/__mocks__/styleMock.js`, + }, } diff --git a/integration-tests/ssr/package.json b/integration-tests/ssr/package.json index f0b0b554ac26b..37b7c68e5e71f 100644 --- a/integration-tests/ssr/package.json +++ b/integration-tests/ssr/package.json @@ -8,8 +8,10 @@ }, "dependencies": { "gatsby": "^2.27.0", + "gatsby-plugin-postcss": "^3.3.0", "react": "^16.12.0", - "react-dom": "^16.12.0" + "react-dom": "^16.12.0", + "tailwindcss": "1" }, "devDependencies": { "cross-env": "^5.0.2", diff --git a/integration-tests/ssr/postcss.config.js b/integration-tests/ssr/postcss.config.js new file mode 100644 index 0000000000000..fd147b48a0bdd --- /dev/null +++ b/integration-tests/ssr/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require("tailwindcss"), require("autoprefixer")], +} \ No newline at end of file diff --git a/integration-tests/ssr/sample.css b/integration-tests/ssr/sample.css new file mode 100644 index 0000000000000..bfbad3af78724 --- /dev/null +++ b/integration-tests/ssr/sample.css @@ -0,0 +1,6 @@ +body { + background: tomato; + color: black; + font-style: italic; + font-weight: 400; +} diff --git a/integration-tests/ssr/src/pages/index.js b/integration-tests/ssr/src/pages/index.js index 850e99d4638bb..d2b3ba51878af 100644 --- a/integration-tests/ssr/src/pages/index.js +++ b/integration-tests/ssr/src/pages/index.js @@ -1,5 +1,6 @@ import React from "react" import { useStaticQuery, graphql } from "gatsby" +const lazyImport = import(`../test`) export default function Inline() { const { site } = useStaticQuery(graphql` @@ -11,5 +12,9 @@ export default function Inline() { } } `) - return
{site.siteMetadata.title}
+ return ( +
+

{site.siteMetadata.title}

+
+ ) } diff --git a/integration-tests/ssr/src/pages/usingtailwind.js b/integration-tests/ssr/src/pages/usingtailwind.js new file mode 100644 index 0000000000000..5baf5467e110b --- /dev/null +++ b/integration-tests/ssr/src/pages/usingtailwind.js @@ -0,0 +1,4 @@ +import React from "react" +import "../styles/tailwind.css" + +export default () =>

This is a 3xl text

diff --git a/integration-tests/ssr/src/styles/tailwind.css b/integration-tests/ssr/src/styles/tailwind.css new file mode 100644 index 0000000000000..7f393742af264 --- /dev/null +++ b/integration-tests/ssr/src/styles/tailwind.css @@ -0,0 +1,5 @@ +@tailwind base; + +@tailwind components; + +@tailwind utilities; diff --git a/integration-tests/ssr/src/test.css b/integration-tests/ssr/src/test.css new file mode 100644 index 0000000000000..7e6738ee67d84 --- /dev/null +++ b/integration-tests/ssr/src/test.css @@ -0,0 +1,3 @@ +.hi { + color: blue; +} diff --git a/integration-tests/ssr/src/test.js b/integration-tests/ssr/src/test.js new file mode 100644 index 0000000000000..1a0ddf56abf67 --- /dev/null +++ b/integration-tests/ssr/src/test.js @@ -0,0 +1 @@ +import "./test.css" diff --git a/integration-tests/ssr/tailwind.config.js b/integration-tests/ssr/tailwind.config.js new file mode 100644 index 0000000000000..de644ca9ab7cf --- /dev/null +++ b/integration-tests/ssr/tailwind.config.js @@ -0,0 +1,10 @@ +module.exports = { + purge: [ + './src/**/*.js', + ], + theme: { + extend: {} + }, + variants: {}, + plugins: [] +} diff --git a/integration-tests/ssr/test-output.js b/integration-tests/ssr/test-output.js index d785a42f32c7e..34664599e4ce4 100644 --- a/integration-tests/ssr/test-output.js +++ b/integration-tests/ssr/test-output.js @@ -21,8 +21,10 @@ const $ = cheerio.load(htmlStr) // There are many script tag differences $(`script`).remove() - // Only added in production. Dev uses css-loader + // Only added in production $(`#gatsby-global-css`).remove() + // Only added in development + $(`link[data-identity='gatsby-dev-css']`).remove() // Only in prod $(`link[rel="preload"]`).remove() // Only in prod diff --git a/packages/gatsby/cache-dir/__tests__/static-entry.js b/packages/gatsby/cache-dir/__tests__/static-entry.js index 4f26a9c6bb6dd..e6c4c1351f650 100644 --- a/packages/gatsby/cache-dir/__tests__/static-entry.js +++ b/packages/gatsby/cache-dir/__tests__/static-entry.js @@ -2,7 +2,6 @@ import React from "react" import fs from "fs" const { join } = require(`path`) -import ssrDevelopStaticEntry from "../ssr-develop-static-entry" import developStaticEntry from "../develop-static-entry" jest.mock(`fs`, () => { @@ -22,7 +21,7 @@ jest.mock( () => { return { ssrComponents: { - "page-component---src-pages-test-js": () => null, + "page-component---src-pages-about-js": () => null, }, } }, @@ -151,8 +150,27 @@ const fakeComponentsPluginFactory = type => { } } +const SSR_DEV_MOCK_FILE_INFO = { + [`${process.cwd()}/public/webpack.stats.json`]: `{}`, + [join( + process.cwd(), + `/public/page-data/about/page-data.json` + )]: JSON.stringify({ + componentChunkName: `page-component---src-pages-about-js`, + path: `/about/`, + webpackCompilationHash: `1234567890abcdef1234`, + staticQueryHashes: [], + }), + [join(process.cwd(), `/public/page-data/app-data.json`)]: JSON.stringify({ + webpackCompilationHash: `1234567890abcdef1234`, + }), +} + describe(`develop-static-entry`, () => { + let ssrDevelopStaticEntry beforeEach(() => { + fs.readFileSync.mockImplementation(file => SSR_DEV_MOCK_FILE_INFO[file]) + ssrDevelopStaticEntry = require(`../ssr-develop-static-entry`).default global.__PATH_PREFIX__ = `` global.__BASE_PATH__ = `` global.__ASSET_PREFIX__ = `` diff --git a/packages/gatsby/cache-dir/ssr-develop-static-entry.js b/packages/gatsby/cache-dir/ssr-develop-static-entry.js index f35cb8a3db0c8..c321d307f0539 100644 --- a/packages/gatsby/cache-dir/ssr-develop-static-entry.js +++ b/packages/gatsby/cache-dir/ssr-develop-static-entry.js @@ -1,7 +1,7 @@ import React from "react" import fs from "fs" import { renderToString, renderToStaticMarkup } from "react-dom/server" -import { merge } from "lodash" +import { get, merge, isObject, flatten, uniqBy, concat } from "lodash" import { join } from "path" import apiRunner from "./api-runner-ssr" import { grabMatchParams } from "./find-path" @@ -20,6 +20,10 @@ const testRequireError = (moduleName, err) => { return regex.test(firstLine) } +const stats = JSON.parse( + fs.readFileSync(`${process.cwd()}/public/webpack.stats.json`, `utf-8`) +) + let Html try { Html = require(`../src/html`) @@ -111,7 +115,66 @@ export default (pagePath, isClientOnlyPage, callback) => { const pageData = getPageData(pagePath) - const componentChunkName = pageData?.componentChunkName + const { componentChunkName, staticQueryHashes = [] } = pageData + + let scriptsAndStyles = flatten( + [`commons`].map(chunkKey => { + const fetchKey = `assetsByChunkName[${chunkKey}]` + + let chunks = get(stats, fetchKey) + const namedChunkGroups = get(stats, `namedChunkGroups`) + + if (!chunks) { + return null + } + + chunks = chunks.map(chunk => { + if (chunk === `/`) { + return null + } + return { rel: `preload`, name: chunk } + }) + + namedChunkGroups[chunkKey].assets.forEach(asset => + chunks.push({ rel: `preload`, name: asset }) + ) + + const childAssets = namedChunkGroups[chunkKey].childAssets + for (const rel in childAssets) { + chunks = concat( + chunks, + childAssets[rel].map(chunk => { + return { rel, name: chunk } + }) + ) + } + + return chunks + }) + ) + .filter(s => isObject(s)) + .sort((s1, s2) => (s1.rel == `preload` ? -1 : 1)) // given priority to preload + + scriptsAndStyles = uniqBy(scriptsAndStyles, item => item.name) + + const styles = scriptsAndStyles.filter( + style => style.name && style.name.endsWith(`.css`) + ) + + styles + .slice(0) + .reverse() + .forEach(style => { + headComponents.unshift( + + ) + }) const createElement = React.createElement diff --git a/packages/gatsby/src/utils/webpack-utils.ts b/packages/gatsby/src/utils/webpack-utils.ts index 869540cc7ee17..77c942504f122 100644 --- a/packages/gatsby/src/utils/webpack-utils.ts +++ b/packages/gatsby/src/utils/webpack-utils.ts @@ -195,12 +195,28 @@ export const createWebpackUtils = ( }, miniCssExtract: (options = {}) => { - return { - options, - // use MiniCssExtractPlugin only on production builds - loader: PRODUCTION - ? MiniCssExtractPlugin.loader - : require.resolve(`style-loader`), + if (PRODUCTION) { + // production always uses MiniCssExtractPlugin + return { + loader: MiniCssExtractPlugin.loader, + options, + } + } else if (process.env.GATSBY_EXPERIMENTAL_DEV_SSR) { + // develop with ssr also uses MiniCssExtractPlugin + return { + loader: MiniCssExtractPlugin.loader, + options: { + ...options, + // enable hmr for browser bundle, ssr bundle doesn't need it + hmr: stage === `develop`, + }, + } + } else { + // develop without ssr is using style-loader + return { + loader: require.resolve(`style-loader`), + options, + } } }, @@ -690,8 +706,6 @@ export const createWebpackUtils = ( plugins.extractText = (options: any): Plugin => new MiniCssExtractPlugin({ - filename: `[name].[contenthash].css`, - chunkFilename: `[name].[contenthash].css`, ...options, }) diff --git a/packages/gatsby/src/utils/webpack.config.js b/packages/gatsby/src/utils/webpack.config.js index 68a5a6ecd1d7c..669d941995a28 100644 --- a/packages/gatsby/src/utils/webpack.config.js +++ b/packages/gatsby/src/utils/webpack.config.js @@ -230,10 +230,21 @@ module.exports = async ( plugins.eslintGraphqlSchemaReload(), ]) .filter(Boolean) + if (process.env.GATSBY_EXPERIMENTAL_DEV_SSR) { + // Don't use the default mini-css-extract-plugin setup as that + // breaks hmr. + configPlugins.push( + plugins.extractText({ filename: `[name].css` }), + plugins.extractStats() + ) + } break case `build-javascript`: { configPlugins = configPlugins.concat([ - plugins.extractText(), + plugins.extractText({ + filename: `[name].[contenthash].css`, + chunkFilename: `[name].[contenthash].css`, + }), // Write out stats object mapping named dynamic imports (aka page // components) to all their async chunks. plugins.extractStats(),