Skip to content

Commit

Permalink
autoamtically use hashing when detecting server side render
Browse files Browse the repository at this point in the history
  • Loading branch information
christianalfoni committed Jul 8, 2020
1 parent faedc1c commit c02e72f
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 51 deletions.
63 changes: 40 additions & 23 deletions packages/css/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createSheets,
cssPropToToken,
getVendorPrefixAndProps,
hashString,
specificityProps,
} from "./utils";

Expand All @@ -40,13 +41,17 @@ const toStringCompose = function (this: IComposedAtom) {
const createToString = (
sheets: { [screen: string]: ISheet },
screens: IScreens = {},
cssClassnameProvider: (seq: number, atom: IAtom) => [string, string?], // [className, pseudo]
startSeq = 0
cssClassnameProvider: (atom: IAtom, seq: number | null) => [string, string?], // [className, pseudo]
preInjectedRules: Set<string>
) => {
let seq = 0;
return function toString(this: IAtom) {
const shouldInject = seq >= startSeq;
const className = cssClassnameProvider(seq++, this);
const className = cssClassnameProvider(
this,
preInjectedRules.size ? null : seq++
);
const shouldInject =
!preInjectedRules.size || !preInjectedRules.has(`.${className[0]}`);
const value = this.value;

if (shouldInject) {
Expand Down Expand Up @@ -81,11 +86,10 @@ const createToString = (
const createServerToString = (
sheets: { [screen: string]: ISheet },
screens: IScreens = {},
cssClassnameProvider: (seq: number, atom: IAtom) => [string, string?] // [className, pseudo]
cssClassnameProvider: (atom: IAtom, seq: number | null) => [string, string?] // [className, pseudo]
) => {
let seq = 0;
return function toString(this: IAtom) {
const className = cssClassnameProvider(seq++, this);
const className = cssClassnameProvider(this, null);
const value = this.value;

let cssRule = "";
Expand Down Expand Up @@ -241,16 +245,26 @@ export const createCss = <T extends IConfig>(
: prefix
: "";
const cssClassnameProvider = (
seq: number,
atom: IAtom
atom: IAtom,
seq: number | null
): [string, string?] => {
const hash =
seq === null
? hashString(
`${atom.screen || ""}${atom.cssHyphenProp.replace(
/-(moz|webkit|ms)-/,
""
)}${atom.pseudo || ""}${atom.value}`
)
: seq;
const name = showFriendlyClassnames
? `${atom.screen ? `${atom.screen}_` : ""}${atom.cssHyphenProp
.replace(/-(moz|webkit|ms)-/, "")
.split("-")
.map((part) => part[0])
.join("")}`
: "";
const className = `${classPrefix}${name}_${seq}`;
.join("")}_${hash}`
: `_${hash}`;
const className = `${classPrefix}${name}`;

if (atom.pseudo) {
return [className, atom.pseudo];
Expand All @@ -260,18 +274,21 @@ export const createCss = <T extends IConfig>(
};

const { tags, sheets } = createSheets(env, config.screens);
const startSeq = Object.keys(sheets)
.filter((key) => key !== "__variables__")
.reduce((count, key) => {
// Can fail with cross origin (like Codesandbox)
try {
return count + sheets[key].cssRules.length;
} catch {
return count;
}
}, 0);
const preInjectedRules = new Set<string>();
// tslint:disable-next-line
for (const sheet in sheets) {
for (let x = 0; x < sheets[sheet].cssRules.length; x++) {
preInjectedRules.add(sheets[sheet].cssRules[x].selectorText);
}
}

let toString = env
? createToString(sheets, config.screens, cssClassnameProvider, startSeq)
? createToString(
sheets,
config.screens,
cssClassnameProvider,
preInjectedRules
)
: createServerToString(sheets, config.screens, cssClassnameProvider);

let themeToString = createThemeToString(classPrefix, sheets.__variables__);
Expand Down
14 changes: 14 additions & 0 deletions packages/css/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,17 @@ export const getVendorPrefixAndProps = (env: any) => {

return { vendorPrefix: `-${vendorPrefix}-`, vendorProps };
};

export const hashString = (str: string) => {
let hash = 5381;
let i = str.length;

while (i) {
hash = (hash * 33) ^ str.charCodeAt(--i);
}

/* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
* integers. Since we want the results to be always positive, convert the
* signed int to an unsigned by doing an unsigned bitshift. */
return hash >>> 0;
};
68 changes: 40 additions & 28 deletions packages/css/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ describe("createCss", () => {
expect(atom.screen).toBe("");
expect(atom.value).toBe("red");
const { styles } = css.getStyles(() => {
expect(atom.toString()).toBe("_0");
expect(atom.toString()).toBe("_1725676875");

return "";
});

expect(styles.length).toBe(2);
expect(styles[1].trim()).toBe("/* STITCHES */\n\n._0{color:red;}");
expect(styles[1].trim()).toBe("/* STITCHES */\n\n._1725676875{color:red;}");
});
test("should compose atoms", () => {
const css = createCss({}, null);
expect(
css.compose(css.color("red"), css.backgroundColor("blue")).toString()
).toBe("_0 _1");
).toBe("_763805413 _1725676875");
});
test("should create tokens", () => {
const tokens = createTokens({
Expand All @@ -92,13 +92,13 @@ describe("createCss", () => {
expect(atom.screen).toBe("");
expect(atom.value).toBe("var(--colors-RED)");
const { styles } = css.getStyles(() => {
expect(atom.toString()).toBe("_0");
expect(atom.toString()).toBe("_3389639116");
return "";
});

expect(styles.length).toBe(2);
expect(styles[1].trim()).toBe(
"/* STITCHES */\n\n._0{color:var(--colors-RED);}"
"/* STITCHES */\n\n._3389639116{color:var(--colors-RED);}"
);
});
test("should create screens", () => {
Expand All @@ -116,13 +116,13 @@ describe("createCss", () => {
expect(atom.pseudo).toBe(undefined);
expect(atom.screen).toBe("tablet");
const { styles } = css.getStyles(() => {
expect(atom.toString()).toBe("_0");
expect(atom.toString()).toBe("_2796359201");
return "";
});

expect(styles.length).toBe(3);
expect(styles[2].trim()).toBe(
"/* STITCHES:tablet */\n\n@media (min-width: 700px) { ._0{color:red;} }"
"/* STITCHES:tablet */\n\n@media (min-width: 700px) { ._2796359201{color:red;} }"
);
});
test("should handle pseudos", () => {
Expand All @@ -133,12 +133,14 @@ describe("createCss", () => {
expect(atom.pseudo).toBe(":hover");
expect(atom.screen).toBe("");
const { styles } = css.getStyles(() => {
expect(atom.toString()).toBe("_0");
expect(atom.toString()).toBe("_627048087");
return "";
});

expect(styles.length).toBe(2);
expect(styles[1].trim()).toBe("/* STITCHES */\n\n._0:hover{color:red;}");
expect(styles[1].trim()).toBe(
"/* STITCHES */\n\n._627048087:hover{color:red;}"
);
});
test("should handle specificity", () => {
const css = createCss({}, null);
Expand All @@ -150,18 +152,18 @@ describe("createCss", () => {
css.backgroundColor("green")
)
.toString()
).toBe("_0 _1");
).toBe("_736532192 _1725676875");
});
test("should insert rule only once", () => {
const css = createCss({}, null);
const { styles } = css.getStyles(() => {
expect(css.color("red").toString()).toBe("_0");
expect(css.color("red").toString()).toBe("_0");
expect(css.color("red").toString()).toBe("_1725676875");
expect(css.color("red").toString()).toBe("_1725676875");
return "";
});

expect(styles.length).toBe(2);
expect(styles[1].trim()).toBe("/* STITCHES */\n\n._0{color:red;}");
expect(styles[1].trim()).toBe("/* STITCHES */\n\n._1725676875{color:red;}");
});
test("should handle specificity with different but same pseudo", () => {
const css = createCss({}, null);
Expand All @@ -172,7 +174,15 @@ describe("createCss", () => {
css.color("red", ":disabled:hover")
)
.toString()
).toBe("_0");
).toBe("_3266759165");
});
test("should use simple sequence for classname when browser", () => {
const fakeEnv = createFakeEnv();
const css = createCss({}, (fakeEnv as unknown) as Window);
String(css.color("red"));
expect(fakeEnv.document.styleSheets[1].cssRules[0].cssText).toBe(
"._0 {color: red;}"
);
});
test("should inject sheet", () => {
const fakeEnv = createFakeEnv();
Expand Down Expand Up @@ -212,14 +222,14 @@ describe("createCss", () => {
},
null
);
expect(css.marginX("1rem").toString()).toBe("_0 _1");
expect(css.marginX("1rem").toString()).toBe("_4081121629 _97196166");
});
test("should ignore undefined atoms", () => {
const css = createCss({}, null);
expect(
// @ts-ignore
String(css.compose(undefined, null, false, "", css.color("red")))
).toBe("_0");
).toBe("_1725676875");
});

test("should allow empty compose call", () => {
Expand All @@ -230,7 +240,7 @@ describe("createCss", () => {
test("should allow conditional compositions", () => {
const css = createCss({}, null);
expect(String(css.compose((false as any) && css.color("red")))).toBe("");
expect(String(css.compose(true && css.color("red")))).toBe("_0");
expect(String(css.compose(true && css.color("red")))).toBe("_1725676875");
});

test("should allow prefixes", () => {
Expand All @@ -243,7 +253,7 @@ describe("createCss", () => {
expect(
// @ts-ignore
String(css.color("red"))
).toBe("foo_0");
).toBe("foo_1725676875");
});
test("should expose override with utility first", () => {
const css = createCss(
Expand All @@ -255,7 +265,7 @@ describe("createCss", () => {
expect(
// @ts-ignore
String(css.override.color("red"))
).toBe("_0");
).toBe("_1725676875");
});
test("should not inject existing styles", () => {
const serverCss = createCss({}, null);
Expand All @@ -271,7 +281,7 @@ describe("createCss", () => {
expect(fakeEnv.document.styleSheets.length).toBe(2);
expect(fakeEnv.document.styleSheets[1].cssRules.length).toBe(1);
expect(fakeEnv.document.styleSheets[1].cssRules[0].cssText).toBe(
"._0 {color: red;}"
"._1725676875 {color: red;}"
);
// On the client it will rerun the logic (React hydrate etc.)
clientCss.color("red").toString();
Expand All @@ -280,7 +290,7 @@ describe("createCss", () => {
// Lets see if it continues on the correct sequence
expect(fakeEnv.document.styleSheets[1].cssRules.length).toBe(2);
expect(fakeEnv.document.styleSheets[1].cssRules[0].cssText).toBe(
"._1 {color: blue;}"
"._1757807590 {color: blue;}"
);
});
test("should be able to show friendly classnames", () => {
Expand All @@ -298,7 +308,7 @@ describe("createCss", () => {

expect(styles).toEqual([
`/* STITCHES:__variables__ */\n\n:root{}`,
`/* STITCHES */\n\n.c_0{color:red;}\n.bc_1{background-color:red;}`,
`/* STITCHES */\n\n.c_1725676875{color:red;}\n.bc_1056962344{background-color:red;}`,
]);
});
test("should inject vendor prefix where explicitly stating so", () => {
Expand All @@ -316,7 +326,7 @@ describe("createCss", () => {

expect(styles).toEqual([
`/* STITCHES:__variables__ */\n\n:root{}`,
`/* STITCHES */\n\n.wc_0{-webkit-color:red;}`,
`/* STITCHES */\n\n.c_1725676875{-webkit-color:red;}`,
]);
});
test("should inject vendor prefix environment defines it", () => {
Expand All @@ -330,7 +340,9 @@ describe("createCss", () => {
});
test("should use specificity props", () => {
const css = createCss({}, null);
expect(String(css.margin("1px"))).toBe("_0 _1 _2 _3");
expect(String(css.margin("1px"))).toBe(
"_2683736640 _968032303 _4032728388 _4031826548"
);
});
test("should have declarative api", () => {
const css = createCss({}, null);
Expand All @@ -339,7 +351,7 @@ describe("createCss", () => {
color: "red",
backgroundColor: "blue",
}).toString()
).toBe("_0 _1");
).toBe("_763805413 _1725676875");
});
test("should handle declarative pseudo selector", () => {
const fakeEnv = createFakeEnv([], []);
Expand Down Expand Up @@ -367,7 +379,7 @@ describe("createCss", () => {

expect(styles.length).toBe(3);
expect(styles[2].trim()).toBe(
"/* STITCHES:mobile */\n\n@media(min-width:700px){._0{color:red;}}"
"/* STITCHES:mobile */\n\n@media(min-width:700px){._2196820011{color:red;}}"
);
});
test("should handle pseudo in screen selector", () => {
Expand All @@ -387,7 +399,7 @@ describe("createCss", () => {

expect(styles.length).toBe(3);
expect(styles[2].trim()).toBe(
"/* STITCHES:mobile */\n\n@media(min-width:700px){._0:hover{color:red;}}"
"/* STITCHES:mobile */\n\n@media(min-width:700px){._860048247:hover{color:red;}}"
);
});
test("should insert themes", () => {
Expand Down Expand Up @@ -419,7 +431,7 @@ describe("createCss", () => {
expect(styles.length).toBe(2);
expect(styles).toEqual([
"/* STITCHES:__variables__ */\n\n:root{--colors-primary:tomato;}\n.theme-0{--colors-primary:blue;}",
"/* STITCHES */\n\n._0{color:var(--colors-primary);}",
"/* STITCHES */\n\n._221333491{color:var(--colors-primary);}",
]);
});
});

0 comments on commit c02e72f

Please sign in to comment.