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 2 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,49 @@
// 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>
eddhurst marked this conversation as resolved.
Show resolved Hide resolved
<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
56 changes: 51 additions & 5 deletions src/server/utils/renderModuleStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,58 @@

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>`;
eddhurst marked this conversation as resolved.
Show resolved Hide resolved

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

const filterOutDuplicateDigests = (sheet, existingDigests) => sheet
.filter(({ css, digest }) => css && digest && !existingDigests.has(digest))
.map((styles) => {
existingDigests.add(styles.digest);
eddhurst marked this conversation as resolved.
Show resolved Hide resolved
return styles;
});
eddhurst marked this conversation as resolved.
Show resolved Hide resolved

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();
if (ssrStylesFullSheet) {
acc.legacy.push({ css: ssrStylesFullSheet });
}
return acc;
eddhurst marked this conversation as resolved.
Show resolved Hide resolved
}

const { aggregatedStyles } = module.ssrStyles;
return {
...acc,
aggregated: [
...acc.aggregated,
...filterOutDuplicateDigests(aggregatedStyles, existingDigests),
],
};
}, { aggregated: [], legacy: [] });
eddhurst marked this conversation as resolved.
Show resolved Hide resolved

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