Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

feat(styleLoader): aggregate stylesheets and dedupe if already loaded #1099

Merged
merged 6 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renderModuleStyles should handle a mix of modules with and without SSR styles 1`] = `"<style class="ssr-css">.test-module-b_class { color: red; }</style><style class="ssr-css">.test-module-d_class { color: red; }</style>"`;
exports[`renderModuleStyles should deduplicate aggregatedStyles that have the same digest hash 1`] = `"<style id="shared_hash_deps" data-ssr="true">.test-module-a_deps { color: blue; }</style><style id="shared_hash_local" data-ssr="true">.test-module-a_local { color: rebeccapurple; }</style><style id="unique-hash_deps" data-ssr="true">.test-module-c_deps { color: blue; }</style><style id="unique-hash_local" data-ssr="true">.test-module-c_local { color: rebeccapurple; }</style>"`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between data-ssr="true' and class="ssr-css" ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strictly speaking none - although a style tag ought not have a class name because we're not applying styles onto the element. The idea here was more of a debug step to allow us to more easily track which styles are being hydrated and which are not. We could dispense with this entirely, but it felt like that might still be a useful step for more easily debugging / visualising the changes.


exports[`renderModuleStyles should handle a mix of modules with and without SSR styles 1`] = `"<style class="ssr-css">.test-module-b_class { color: red; }</style><style class="ssr-css">.test-module-d_class { color: red; }</style><style id="test-module-e_deps" data-ssr="true">.test-module-e_deps { color: blue; }</style><style id="test-module-e_local" data-ssr="true">.test-module-e_local { color: rebeccapurple; }</style>"`;

exports[`renderModuleStyles should handle modules with SSR styles 1`] = `"<style class="ssr-css">.test-module_class { color: red; }</style>"`;

exports[`renderModuleStyles should not send empty styles 1`] = `"<style class="ssr-css">.test-module-b_class { color: red; }</style>"`;
exports[`renderModuleStyles should handle modules with aggregated SSR styles 1`] = `"<style id="test-module_deps" data-ssr="true">.test-module_deps { color: blue; }</style><style id="test-module_local" data-ssr="true">.test-module_local { color: rebeccapurple; }</style>"`;

exports[`renderModuleStyles should not send empty styles 1`] = `"<style class="ssr-css">.test-module-b_class { color: red; }</style><style id="test-module-d_deps" data-ssr="true">.test-module-d_deps { color: blue; }</style><style id="test-module-d_local" data-ssr="true">.test-module-d_local { color: rebeccapurple; }</style>"`;
68 changes: 66 additions & 2 deletions __tests__/server/utils/renderModuleStyles.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,43 @@
import { Map, Set as iSet, fromJS } from 'immutable';
import renderModuleStyles from '../../../src/server/utils/renderModuleStyles';

const getModuleWithMisconfiguredAggregatedStyles = (
moduleName,
includeCSS = true,
includeDigest = true
) => {
const moduleWithMisconfiguredAggregatedStyles = () => 0;

const css = includeCSS && `.${moduleName}_deps { color: blue; }`;
const digest = includeDigest && `${moduleName}_hash`;

moduleWithMisconfiguredAggregatedStyles.ssrStyles = {
aggregatedStyles: [{ css, digest }],
getFullSheet: () => `.${moduleName}_class { color: red; }`,
};
return moduleWithMisconfiguredAggregatedStyles;
};

const getModuleWithAggregatedStyles = (moduleName, digest) => {
const moduleWithAggregatedStyles = () => 0;
moduleWithAggregatedStyles.ssrStyles = {
aggregatedStyles: [
{ css: `.${moduleName}_deps { color: blue; }`, digest: `${digest || moduleName}_deps` },
{ css: `.${moduleName}_local { color: rebeccapurple; }`, digest: `${digest || moduleName}_local` },
],
getFullSheet: () => `.${moduleName}_class { color: red; }`,
};
return moduleWithAggregatedStyles;
};

const getModuleWithStyles = (moduleName) => {
const moduleWithStyles = () => 0;
moduleWithStyles.ssrStyles = {
getFullSheet: () => `.${moduleName}_class { color: red; }`,
};
return moduleWithStyles;
};

const moduleWithoutStyles = () => 0;

describe('renderModuleStyles', () => {
Expand All @@ -48,28 +78,61 @@ describe('renderModuleStyles', () => {
expect(renderModuleStyles(store)).toMatchSnapshot();
});

it('should handle modules with aggregated SSR styles', () => {
const state = fromJS({ holocron: { loaded: iSet(['test-module']) } });
const modules = new Map({ 'test-module': getModuleWithAggregatedStyles('test-module') });
const store = { getState: () => state, modules };
expect(renderModuleStyles(store)).toMatchSnapshot();
});

it('should handle modules with misconfigured aggregated SSR styles', () => {
const state = fromJS({ holocron: { loaded: iSet(['no-css', 'no-digest']) } });
const modules = new Map({
'no-css': getModuleWithMisconfiguredAggregatedStyles('no-css', false, true),
'no-digest': getModuleWithMisconfiguredAggregatedStyles('no-digest', true, false),
});
const store = { getState: () => state, modules };
expect(renderModuleStyles(store)).toBe('');
});

it('should handle a mix of modules with and without SSR styles', () => {
const state = fromJS({
holocron: {
loaded: iSet(['test-module-a', 'test-module-b', 'test-module-c', 'test-module-d']),
loaded: iSet(['test-module-a', 'test-module-b', 'test-module-c', 'test-module-d', 'test-module-e']),
},
});
const modules = new Map({
'test-module-a': moduleWithoutStyles,
'test-module-b': getModuleWithStyles('test-module-b'),
'test-module-c': moduleWithoutStyles,
'test-module-d': getModuleWithStyles('test-module-d'),
'test-module-e': getModuleWithAggregatedStyles('test-module-e'),
});
const store = { getState: () => state, modules };
expect(renderModuleStyles(store)).toMatchSnapshot();
});

it('should not send empty styles', () => {
it('should deduplicate aggregatedStyles that have the same digest hash', () => {
const state = fromJS({
holocron: {
loaded: iSet(['test-module-a', 'test-module-b', 'test-module-c']),
},
});
const modules = new Map({
'test-module-a': getModuleWithAggregatedStyles('test-module-a', 'shared_hash'),
'test-module-b': getModuleWithAggregatedStyles('test-module-b', 'shared_hash'),
'test-module-c': getModuleWithAggregatedStyles('test-module-c', 'unique-hash'),
});
const store = { getState: () => state, modules };
expect(renderModuleStyles(store)).toMatchSnapshot();
});

it('should not send empty styles', () => {
const state = fromJS({
holocron: {
loaded: iSet(['test-module-a', 'test-module-b', 'test-module-c', 'test-module-d']),
},
});
const moduleWithEmptyStyles = () => 0;
moduleWithEmptyStyles.ssrStyles = {
getFullSheet: () => undefined,
Expand All @@ -78,6 +141,7 @@ describe('renderModuleStyles', () => {
'test-module-a': moduleWithoutStyles,
'test-module-b': getModuleWithStyles('test-module-b'),
'test-module-c': moduleWithEmptyStyles,
'test-module-d': getModuleWithAggregatedStyles('test-module-d'),
});
const store = { getState: () => state, modules };
expect(renderModuleStyles(store)).toMatchSnapshot();
Expand Down
60 changes: 55 additions & 5 deletions src/server/utils/renderModuleStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,62 @@

import { getModule } from 'holocron';

/**
* Generates a style tag with unique ID attribute per CSS file loaded.
* @returns {string}
*/
const generateStyleTag = ({ css, digest }) => `<style id="${digest}" data-ssr="true">${css}</style>`;

const generateServerStyleTag = (css) => `<style class="ssr-css">${css}</style>`;

const filterOutDuplicateDigests = (sheet, existingDigests) => sheet
.filter(({ css, digest }) => css && digest && !existingDigests.has(digest));

const updateExistingDigests = (filteredSheets, existingDigests) => filteredSheets
.forEach((sheet) => { existingDigests.add(sheet.digest); });

export default function renderModuleStyles(store) {
return store.getState().getIn(['holocron', 'loaded'], [])
const existingDigests = new Set();
const modulesWithSSRStyles = store.getState().getIn(['holocron', 'loaded'], [])
.map((moduleName) => getModule(moduleName, store.modules))
.filter((module) => !!module.ssrStyles)
.map((module) => module.ssrStyles.getFullSheet())
.filter((module) => !!module.ssrStyles);

const collatedStyles = modulesWithSSRStyles
.reduce((acc, module) => {
// Backwards compatibility for older bundles.
if (!module.ssrStyles.aggregatedStyles) {
const ssrStylesFullSheet = module.ssrStyles.getFullSheet();
return {
...acc,
legacy: ssrStylesFullSheet
? [
...acc.legacy,
{ css: ssrStylesFullSheet },
]
: acc.legacy,
};
}

const { aggregatedStyles } = module.ssrStyles;
const uniqueStyles = filterOutDuplicateDigests(aggregatedStyles, existingDigests);
updateExistingDigests(uniqueStyles, existingDigests);

return {
...acc,
aggregated: [
...acc.aggregated,
...uniqueStyles,
],
};
}, { aggregated: [], legacy: [] });

return [...collatedStyles.legacy, ...collatedStyles.aggregated]
.filter(Boolean)
.map((ssrStylesFullSheet) => `<style class="ssr-css">${ssrStylesFullSheet}</style>`)
.join('');
.reduce((acc, { css, digest }) => {
if (!digest) {
return acc + generateServerStyleTag(css);
}

return acc + generateStyleTag({ css, digest });
}, '');
}
Loading