Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SX: implement styles rehydration #4039

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/abacus-kochka/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
[![Crowdin](https://badges.crowdin.net/kochkacommx/localized.svg)](https://crowdin.com/project/kochkacommx)

- https://kochka.com.mx/
- [PageSpeed Insights](https://pagespeed.web.dev/report?url=https%3A%2F%2Fkochka.com.mx%2Fmenu&form_factor=mobile)

```text
yarn install
yarn dev
Expand Down
8 changes: 4 additions & 4 deletions src/abacus-kochka/pages/_document.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ type RenderPageResult = {

export default class MyDocument extends Document {
// See: https://nextjs.org/docs/advanced-features/custom-document#customizing-renderpage
static async getInitialProps({ renderPage }: DocumentContext): Promise<RenderPageResult> {
const page = await renderPage();
return { ...page, styles: [sx.getStyleTag()] };
}
// static async getInitialProps({ renderPage }: DocumentContext): Promise<RenderPageResult> {
// const page = await renderPage();
// return { ...page, styles: [sx.getStyleTag()] };
// }

render(): Node {
return (
Expand Down
9 changes: 9 additions & 0 deletions src/sx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ In conventional applications, CSS rules are duplicated throughout the stylesheet
- [Automatic vendor prefixes](#automatic-vendor-prefixes)
- [Server-side rendering](#server-side-rendering)
- [Architecture](#architecture)
- [Runtime styles architecture](#runtime-styles-architecture)
- [Prior Art](#prior-art)

## Installation and Usage
Expand Down Expand Up @@ -480,6 +481,14 @@ Internally, these steps are happening:

5. and finally, we collect the values of the final object and print them as `className`

### Runtime styles architecture

Runtime styles are styles that were not rendered by server (are for whatever reason missing or SSR is not enabled). Here is how SX deals with this situation:

1. SX tries to find `<style data-adeira-sx />` (or creates it if it doesn't exist yet) and does "rehydration" where it goes through the existing styles and remembers which ones are already applied.
2. SX performs runtime injection of the styles while checking whether the styles already exist or not.
3. The rest is the same.

## Prior Art

_sorted alphabetically_
Expand Down
27 changes: 11 additions & 16 deletions src/sx/src/StyleCollector.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { invariant, warning } from '@adeira/js';

import expandShorthandProperties from './expandShorthandProperties';
import printNodes from './printNodes';
import StyleCollectorAtNode from './StyleCollectorAtNode';
import StyleCollectorPseudoNode from './StyleCollectorPseudoNode';
import { type StyleCollectorNodeInterface } from './StyleCollectorNodeInterface';
Expand Down Expand Up @@ -41,7 +42,7 @@ class StyleCollector {
#styleBuffer: StyleBufferType = new Map();
#keyframes: Map<string, string> = new Map();

collect(baseStyleSheet: { +[sheetName: string]: $FlowFixMe }): {
collectStylesheets(baseStyleSheet: { +[sheetName: string]: $FlowFixMe }): {
+hashRegistry: HashRegistryType,
+styleBuffer: StyleBufferType,
} {
Expand Down Expand Up @@ -106,23 +107,17 @@ class StyleCollector {
};
}

print(): string {
let sxStyle = '';
this.#styleBuffer.forEach((node) => {
sxStyle += node.printNodes().join('');
});
this.#keyframes.forEach((node) => {
sxStyle += node;
});
return sxStyle;
collectKeyframe(name: string, value: string): void {
this.#keyframes.set(name, value);
}

addKeyframe(name: string, value: string): boolean {
if (this.#keyframes.has(name)) {
return true;
}
this.#keyframes.set(name, value);
return false;
// TODO: remove
print(): string {
// TODO: print keyframes as well
// this.#keyframes.forEach((node) => {
// sxStyle += node;
// });
return printNodes([...this.#styleBuffer]);
}

reset(): void {
Expand Down
12 changes: 4 additions & 8 deletions src/sx/src/StyleCollectorAtNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,11 @@ export default class StyleCollectorAtNode implements StyleCollectorNodeInterface
this.nodes = new Map([...this.nodes, ...nodes]);
}

getAtRuleName(): string {
return this.atRuleName;
getNodes() {
return this.nodes;
}

printNodes(config?: PrintConfig): $ReadOnlyArray<string> {
let output = '';
this.nodes.forEach((node) => {
output += node.printNodes({ ...config, bumpSpecificity: true }).join('');
});
return [`${this.atRuleName}{${output}}`];
getAtRuleName(): string {
return this.atRuleName;
}
}
14 changes: 8 additions & 6 deletions src/sx/src/StyleCollectorNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export default class StyleCollectorNode implements StyleCollectorNodeInterface {

// eslint-disable-next-line no-unused-vars
addNodes(nodes: Map<string, StyleCollectorNodeInterface>) {
invariant(false, 'StyleCollectorNode cannot have nested nodes,');
invariant(false, 'StyleCollectorNode cannot have nested nodes.');
}

getNodes(): Map<string, StyleCollectorNode> {
invariant(false, 'StyleCollectorNode cannot have nested nodes.');
}

getHash(): string {
Expand All @@ -52,10 +56,8 @@ export default class StyleCollectorNode implements StyleCollectorNodeInterface {
return this.styleValue;
}

printNodes(config?: PrintConfig): $ReadOnlyArray<string> {
const className = `.${this.hash}`.repeat(config?.bumpSpecificity === true ? 2 : 1);
const pseudo = config?.pseudo ?? '';

return [`${className}${pseudo}{${this.styleName}:${this.styleValue}}`];
// CSSStyleRule.selectorText
rehydrationIdentifier() {
return `.${this.getHash()}`;
}
}
2 changes: 1 addition & 1 deletion src/sx/src/StyleCollectorNodeInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export type PrintConfig = {

export interface StyleCollectorNodeInterface {
addNodes(nodes: Map<string, StyleCollectorNodeInterface>): void;
printNodes(config?: PrintConfig): $ReadOnlyArray<string>;
getNodes(): Map<string, StyleCollectorNodeInterface>;
}
33 changes: 23 additions & 10 deletions src/sx/src/StyleCollectorPseudoNode.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @flow strict

import type { AllCSSPseudoTypes } from './css-properties/__generated__/AllCSSPseudoTypes';
import StyleCollectorNode from './StyleCollectorNode';
import type { PrintConfig, StyleCollectorNodeInterface } from './StyleCollectorNodeInterface';

/**
Expand All @@ -23,29 +25,40 @@ import type { PrintConfig, StyleCollectorNodeInterface } from './StyleCollectorN
* ],
* }
* ```
*
* Note that nesting pseudo classes is not allowed in SX.
*/
export default class StyleCollectorPseudoNode implements StyleCollectorNodeInterface {
pseudo: string;
nodes: Map<string, StyleCollectorNodeInterface>;
nodes: Map<string, StyleCollectorNode>;

constructor(pseudo: string, nodes: Map<string, StyleCollectorNodeInterface>) {
constructor(pseudo: $Keys<AllCSSPseudoTypes>, nodes: Map<string, StyleCollectorNode>) {
this.pseudo = pseudo;
this.nodes = nodes;
}

addNodes(nodes: Map<string, StyleCollectorNodeInterface>) {
addNodes(nodes: Map<string, StyleCollectorNode>) {
this.nodes = new Map([...this.nodes, ...nodes]);
}

getNodes(): Map<string, StyleCollectorNode> {
return this.nodes;
}

getPseudo(): string {
return this.pseudo;
}

printNodes(config?: PrintConfig): $ReadOnlyArray<string> {
let output = [];
this.nodes.forEach((node) => {
output = output.concat(node.printNodes({ ...config, pseudo: this.pseudo }));
});
return output;
}
// printNodes(config?: PrintConfig): $ReadOnlyArray<string> {
// let output = [];
// this.nodes.forEach((node) => {
// output = output.concat(node.printNodes({ ...config, pseudo: this.pseudo }));
// });
// return output;
// }

// CSSStyleRule.selectorText
// rehydrationIdentifier() {
// return `.${this.getHash()}`;
// }
}
78 changes: 78 additions & 0 deletions src/sx/src/__tests__/printNodes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @flow

import printNodes from '../printNodes';
import StyleCollectorAtNode from '../StyleCollectorAtNode';
import StyleCollectorNode from '../StyleCollectorNode';
import StyleCollectorPseudoNode from '../StyleCollectorPseudoNode';

it('prints `StyleCollectorNode` as expected', () => {
expect(
printNodes([
new StyleCollectorNode('color', 'red'),
new StyleCollectorNode('color', 'green'),
new StyleCollectorNode('color', 'blue'),
]),
).toMatchInlineSnapshot(`
"._324Crd{color:#f00}
.mRoJ3{color:#008000}
._2dHaKY{color:#00f}"
`);
});

it('prints `StyleCollectorPseudoNode` as expected', () => {
expect(
printNodes([
new StyleCollectorPseudoNode(
':hover',
new Map([
// TODO: are the map keys necessary (?)
['c0', new StyleCollectorNode('color', 'red')],
['c1', new StyleCollectorNode('color', 'green')],
['c2', new StyleCollectorNode('color', 'blue')],
]),
),
]),
).toMatchInlineSnapshot(`
"._324Crd:hover{color:#f00}
.mRoJ3:hover{color:#008000}
._2dHaKY:hover{color:#00f}"
`);
});

it('prints `StyleCollectorAtNode` as expected', () => {
expect(
printNodes([
new StyleCollectorAtNode(
'@media screen',
new Map([
['c0', new StyleCollectorNode('fontSize', '14')],
[
'c1',
new StyleCollectorPseudoNode(
':hover',
new Map([['c0', new StyleCollectorNode('color', 'pink')]]),
),
],

[
'c2',
new StyleCollectorAtNode(
'@media (max-width: 12cm)',
new Map([['c0', new StyleCollectorNode('color', 'blue')]]),
),
],
]),
),
]),
).toMatchInlineSnapshot(`
"@media screen{
._1fVgat._1fVgat{font-size:14}
._3ncx7d._3ncx7d:hover{color:#ffc0cb}
@media (max-width: 12cm){
._2dHaKY._2dHaKY{color:#00f}
}
}"
`);
});

// TODO: prefixing
58 changes: 58 additions & 0 deletions src/sx/src/__tests__/rehydrateStyles.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @flow
* @jest-environment jsdom
*/

/* global document */

import { invariant } from '@adeira/js';

import rehydrateStyles from '../rehydrateStyles';

it('correctly rehydrates styles from a style element', () => {
// First, we need to create a `CSSStyleSheet` by actually creating a style element:
const styleElement = document.createElement('style');
document.head?.appendChild(styleElement);
const styleSheet = styleElement.sheet;

invariant(styleSheet != null, 'Unable to create test StyleSheet.');

// Insert some simple CSS rules:
styleSheet.insertRule('._2tPCgL { font-size: 10px; }', 0);
styleSheet.insertRule('._1Kmfck:hover { color: rgba(var(--sx-foreground), 0.5); }', 1);

// Insert some @at rules:
styleSheet.insertRule(
`@media (prefers-reduced-motion: reduce) { .VdrO3.VdrO3 { animation-duration: 1s; } }`,
2,
);
styleSheet.insertRule(
`@media (prefers-reduced-motion: reduce) { ._2tPCgL._2tPCgL { font-size: 10px; } }`,
3,
);
styleSheet.insertRule(
`@keyframes oxCh9 { 33% { transform: translateY(-10px); } 66% { transform: translateY(10px); } }
`,
4,
);

// We should be able to decide whether the style needs to be injected later based on the
// following information:
expect(rehydrateStyles(styleSheet)).toMatchInlineSnapshot(`
Object {
"rehydratedKeyframeRules": Set {
"oxCh9",
},
"rehydratedMediaRules": Map {
"(prefers-reduced-motion: reduce)" => Set {
".VdrO3.VdrO3",
"._2tPCgL._2tPCgL",
},
},
"rehydratedStyleRules": Set {
"._2tPCgL",
"._1Kmfck:hover",
},
}
`);
});
8 changes: 6 additions & 2 deletions src/sx/src/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import { invariant, isBrowser, isObjectEmpty, isObject } from '@adeira/js';
import levenshtein from 'fast-levenshtein';

import getStyleSheetFromStyleTag from './getStyleSheetFromStyleTag';
import injectRuntimeStyles from './injectRuntimeStyles';
import rehydrateStyles from './rehydrateStyles';
import styleCollector from './StyleCollector';
import type { AllCSSPropertyTypes } from './css-properties/__generated__/AllCSSPropertyTypes';
import type { AllCSSPseudoTypes } from './css-properties/__generated__/AllCSSPseudoTypes';
Expand Down Expand Up @@ -48,10 +50,12 @@ export default function create<T: SheetDefinitions>(sheetDefinitions: T): Create
`Function 'sx.create' cannot be called with empty stylesheet definition.`,
);

const { hashRegistry, styleBuffer } = styleCollector.collect(sheetDefinitions);
const { hashRegistry, styleBuffer } = styleCollector.collectStylesheets(sheetDefinitions);

if (isBrowser()) {
injectRuntimeStyles(styleBuffer);
const styleSheet = getStyleSheetFromStyleTag();
const rehydratedRules = rehydrateStyles(styleSheet);
injectRuntimeStyles(styleSheet, rehydratedRules, styleBuffer);
}

function sxFunction(maybeObject, ...styleSheetsSelectors) {
Expand Down