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

Commit

Permalink
feat(styleLoader): aggregate stylesheets and dedupe if already loaded (
Browse files Browse the repository at this point in the history
…#1099)

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

* feat(moduleStyles): refactor side effect to dedicated loop function

* chore(moduleStyles): make returns consistent, remove newlines
  • Loading branch information
eddhurst committed Sep 5, 2023
1 parent ac677bf commit a74a5ba
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 9 deletions.
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>"`;

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 });
}, '');
}

0 comments on commit a74a5ba

Please sign in to comment.