summaryrefslogtreecommitdiff
path: root/@linaria/packages/server
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-08-23 16:46:06 -0300
committerSebastian <sebasjm@gmail.com>2021-08-23 16:48:30 -0300
commit38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch)
tree453dbf70000cc5e338b06201af1eaca8343f8f73 /@linaria/packages/server
parentf26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff)
downloadnode-vendor-master.tar.gz
node-vendor-master.tar.bz2
node-vendor-master.zip
added web vendorsHEADmaster
Diffstat (limited to '@linaria/packages/server')
-rw-r--r--@linaria/packages/server/CHANGELOG.md8
-rw-r--r--@linaria/packages/server/README.md35
-rw-r--r--@linaria/packages/server/__tests__/__snapshots__/collect.test.ts.snap300
-rw-r--r--@linaria/packages/server/__tests__/collect.test.ts235
-rw-r--r--@linaria/packages/server/babel.config.js3
-rw-r--r--@linaria/packages/server/package.json46
-rw-r--r--@linaria/packages/server/src/collect.ts115
-rw-r--r--@linaria/packages/server/src/index.ts1
-rw-r--r--@linaria/packages/server/tsconfig.json7
9 files changed, 750 insertions, 0 deletions
diff --git a/@linaria/packages/server/CHANGELOG.md b/@linaria/packages/server/CHANGELOG.md
new file mode 100644
index 0000000..2596d9e
--- /dev/null
+++ b/@linaria/packages/server/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [3.0.0-beta.3](https://github.com/callstack/linaria/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2021-04-20)
+
+**Note:** Version bump only for package @linaria/server
diff --git a/@linaria/packages/server/README.md b/@linaria/packages/server/README.md
new file mode 100644
index 0000000..0d75b37
--- /dev/null
+++ b/@linaria/packages/server/README.md
@@ -0,0 +1,35 @@
+<p align="center">
+ <img alt="Linaria" src="https://raw.githubusercontent.com/callstack/linaria/HEAD/website/assets/linaria-logo@2x.png" width="496">
+</p>
+
+<p align="center">
+Zero-runtime CSS in JS library.
+</p>
+
+---
+
+### 📖 Please refer to the [GitHub](https://github.com/callstack/linaria#readme) for full documentation.
+
+## Features
+
+- Write CSS in JS, but with **zero runtime**, CSS is extracted to CSS files during build
+- Familiar **CSS syntax** with Sass like nesting
+- Use **dynamic prop based styles** with the React bindings, uses CSS variables behind the scenes
+- Easily find where the style was defined with **CSS sourcemaps**
+- **Lint your CSS** in JS with [stylelint](https://github.com/stylelint/stylelint)
+- Use **JavaScript for logic**, no CSS preprocessor needed
+- Optionally use any **CSS preprocessor** such as Sass or PostCSS
+
+**[Why use Linaria](../../docs/BENEFITS.md)**
+
+## Installation
+
+```sh
+npm install @linaria/core @linaria/react @linaria/babel-preset @linaria/shaker
+```
+
+or
+
+```sh
+yarn add @linaria/core @linaria/react @linaria/babel-preset @linaria/shaker
+```
diff --git a/@linaria/packages/server/__tests__/__snapshots__/collect.test.ts.snap b/@linaria/packages/server/__tests__/__snapshots__/collect.test.ts.snap
new file mode 100644
index 0000000..c5ff639
--- /dev/null
+++ b/@linaria/packages/server/__tests__/__snapshots__/collect.test.ts.snap
@@ -0,0 +1,300 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`classname in @rule critical 1`] = `
+"@supports (object-fit: cover) {
+ .linaria {
+ }
+}
+@media (min-width: 600px) {
+ .linaria {
+ }
+}
+@charset() {
+ .linaria {
+ }
+}
+@import() {
+ .linaria {
+ }
+}
+@namespace () {
+ .linaria {
+ }
+}
+@media() {
+ .linaria {
+ }
+}
+@supports () {
+ .linaria {
+ }
+}
+@document() {
+ .linaria {
+ }
+}
+@page() {
+ .linaria {
+ }
+}
+@viewport() {
+ .linaria {
+ }
+}
+@counter-style() {
+ .linaria {
+ }
+}
+@font-feature-values() {
+ .linaria {
+ }
+}
+"
+`;
+
+exports[`classname in @rule other 1`] = `
+"@supports (object-fit: cover) {
+ .other {
+ }
+}
+@media (min-width: 600px) {
+ .other {
+ }
+}
+@charset() {
+ .other {
+ }
+}
+@import() {
+ .other {
+ }
+}
+@namespace () {
+ .other {
+ }
+}
+@media() {
+ .other {
+ }
+}
+@supports () {
+ .other {
+ }
+}
+@document() {
+ .other {
+ }
+}
+@page() {
+ .other {
+ }
+}
+@viewport() {
+ .other {
+ }
+}
+@counter-style() {
+ .other {
+ }
+}
+@font-feature-values() {
+ .other {
+ }
+}
+"
+`;
+
+exports[`collects complex css critical 1`] = `
+".lotus {
+ vertical-align: top;
+}
+@media (max-width: 1200px) {
+ .lotus {
+ vertical-align: bottom;
+ }
+}
+@supports (object-fit: contain) {
+ .lotus {
+ object-fit: contain;
+ }
+
+ .linaria::before,
+ .linaria::after {
+ content: \\"\\";
+ object-fit: contain;
+ }
+}
+.linaria {
+ float: left;
+ flex: 1;
+ animation: custom-animation 0.2s;
+}
+.linaria:hover {
+ float: right;
+}
+.linaria > span,
+.linaria + .linaria,
+.linaria ~ div {
+ display: none;
+}
+.linaria > span {
+ display: none;
+}
+.linaria::after {
+ display: block;
+}
+.lily {
+ color: #fff;
+}
+[data-theme=\\"dark\\"] .lily {
+ color: #000;
+}
+.linaria ~ div {
+}
+.linaria.linaria2 {
+}
+@keyframes custom-animation {
+ 0% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+"
+`;
+
+exports[`collects complex css other 1`] = `
+"@supports (object-fit: cover) {
+ .unrelated-nested {
+ float: left;
+ animation: custom-animation;
+ }
+
+ .unrelated-nested2 {
+ float: left;
+ }
+}
+
+.unrelated {
+ animation-name: custom-animation;
+}
+
+.unrelated2 {
+ animation: custom-animation 0.3s;
+}
+
+.unrelated3 {
+ flex: 0;
+}
+"
+`;
+
+exports[`handles top-level @font-face critical 1`] = `
+"@font-face {
+ font-family: MyFont;
+ font-weight: normal;
+ font-style: normal;
+ src: url(MyFont.woff);
+}
+"
+`;
+
+exports[`handles top-level @font-face other 1`] = `""`;
+
+exports[`include atrule once critical 1`] = `
+"@media screen {
+ body {
+ font-size: 10px;
+ }
+ h1 {
+ font-size: 20px;
+ }
+ .class {
+ font-size: 15px;
+ }
+}
+"
+`;
+
+exports[`include atrule once other 1`] = `""`;
+
+exports[`simple class name critical 1`] = `
+".linaria {
+}
+"
+`;
+
+exports[`simple class name other 1`] = `
+".classname {
+}
+"
+`;
+
+exports[`works with CSS combinators critical 1`] = `
+".linaria + span {
+}
+.linaria ~ div {
+}
+.linaria > a {
+}
+.linaria b {
+}
+"
+`;
+
+exports[`works with CSS combinators other 1`] = `
+".other + span {
+}
+.other ~ div {
+}
+.other > a {
+}
+.other b {
+}
+"
+`;
+
+exports[`works with global css critical 1`] = `
+"body {
+ font-size: 13.37px;
+}
+html {
+ -webkit-font-smoothing: antialiased;
+}
+h1 {
+ font-weight: bold;
+}
+.linaria:active {
+}
+.linaria::before {
+}
+"
+`;
+
+exports[`works with global css other 1`] = `
+".other:active {
+}
+.other::before {
+}
+"
+`;
+
+exports[`works with pseudo-class and pseudo-elements critical 1`] = `
+".linaria:active {
+}
+.linaria::before {
+}
+"
+`;
+
+exports[`works with pseudo-class and pseudo-elements other 1`] = `
+".other:active {
+}
+.other::before {
+}
+"
+`;
diff --git a/@linaria/packages/server/__tests__/collect.test.ts b/@linaria/packages/server/__tests__/collect.test.ts
new file mode 100644
index 0000000..ba5208e
--- /dev/null
+++ b/@linaria/packages/server/__tests__/collect.test.ts
@@ -0,0 +1,235 @@
+import dedent from 'dedent';
+import prettier from 'prettier';
+import collect from '../src/collect';
+
+const prettyPrint = (src: string) => prettier.format(src, { parser: 'scss' });
+
+const testCollect = (html: string, css: string) => {
+ const { critical, other } = collect(html, css);
+ test('critical', () => expect(prettyPrint(critical)).toMatchSnapshot());
+ test('other', () => expect(prettyPrint(other)).toMatchSnapshot());
+};
+
+const html = dedent`
+ <div class="linaria lily">
+ <span class="lotus"></div>
+ </div>
+`;
+
+describe('collects complex css', () => {
+ const css = dedent`
+ .lotus {
+ vertical-align: top;
+ }
+
+ @media (max-width: 1200px) {
+ .lotus {
+ vertical-align: bottom;
+ }
+ }
+
+ @supports (object-fit: cover) {
+ .unrelated-nested {
+ float: left;
+ animation: custom-animation;
+ }
+
+ .unrelated-nested2 {
+ float: left;
+ }
+ }
+
+ @supports (object-fit: contain) {
+ .lotus {
+ object-fit: contain;
+ }
+
+ .linaria::before,
+ .linaria::after {
+ content: '';
+ object-fit: contain;
+ }
+ }
+
+ @keyframes custom-animation {
+ 0% { opacity: 0 }
+ 50% { opacity: 0 }
+ 100% { opacity: 1 }
+ }
+
+ .linaria {
+ float: left;
+ flex: 1;
+ animation: custom-animation .2s;
+ }
+
+ .linaria:hover {
+ float: right;
+ }
+
+ .linaria > span,
+ .linaria + .linaria,
+ .linaria ~ div {
+ display: none;
+ }
+
+ .linaria > span {
+ display: none;
+ }
+
+ .linaria::after {
+ display: block;
+ }
+
+ .unrelated {
+ animation-name: custom-animation;
+ }
+
+ .unrelated2 {
+ animation: custom-animation .3s;
+ }
+
+ .lily {
+ color: #fff;
+ }
+
+ [data-theme=dark] .lily {
+ color: #000;
+ }
+
+ .unrelated3 {
+ flex: 0;
+ }
+
+ .linaria ~ div {}
+ .linaria.linaria2{}
+ `;
+
+ testCollect(html, css);
+});
+
+describe('simple class name', () => {
+ const css = dedent`
+ .linaria {}
+ .classname {}
+ `;
+
+ testCollect(html, css);
+});
+
+describe('classname in @rule', () => {
+ const css = dedent`
+ @supports (object-fit: cover) { .linaria {} }
+ @media (min-width: 600px) { .linaria {} }
+ @charset () { .linaria {} }
+ @import () { .linaria {} }
+ @namespace () { .linaria {} }
+ @media () { .linaria {} }
+ @supports () { .linaria {} }
+ @document () { .linaria {} }
+ @page () { .linaria {} }
+ @keyframes () { .linaria {} }
+ @viewport () { .linaria {} }
+ @counter-style () { .linaria {} }
+ @font-feature-values () { .linaria {} }
+
+ @supports (object-fit: cover) { .other {} }
+ @media (min-width: 600px) { .other {} }
+ @charset () { .other {} }
+ @import () { .other {} }
+ @namespace () { .other {} }
+ @media () { .other {} }
+ @supports () { .other {} }
+ @document () { .other {} }
+ @page () { .other {} }
+ @keyframes () { .other {} }
+ @viewport () { .other {} }
+ @counter-style () { .other {} }
+ @font-feature-values () { .other {} }
+ `;
+
+ testCollect(html, css);
+});
+
+describe('works with CSS combinators', () => {
+ const css = dedent`
+ .linaria + span {}
+ .linaria ~ div {}
+ .linaria > a {}
+ .linaria b {}
+
+ .other + span {}
+ .other ~ div {}
+ .other > a {}
+ .other b {}
+ `;
+ testCollect(html, css);
+});
+
+describe('works with pseudo-class and pseudo-elements', () => {
+ const css = dedent`
+ .linaria:active {}
+ .linaria::before {}
+
+ .other:active {}
+ .other::before {}
+ `;
+ testCollect(html, css);
+});
+
+describe('works with global css', () => {
+ const css = dedent`
+ body { font-size: 13.37px; }
+
+ html { -webkit-font-smoothing: antialiased; }
+
+ h1 { font-weight: bold; }
+
+ .linaria:active {}
+ .linaria::before {}
+
+ .other:active {}
+ .other::before {}
+ `;
+
+ const { critical, other } = collect(html, css);
+
+ test('critical', () => expect(prettyPrint(critical)).toMatchSnapshot());
+ test('other', () => expect(prettyPrint(other)).toMatchSnapshot());
+});
+
+describe('handles top-level @font-face', () => {
+ const css = dedent`
+ @font-face {
+ font-family: MyFont;
+ font-weight: normal;
+ font-style: normal;
+ src: url(MyFont.woff);
+ }
+ `;
+ const { critical, other } = collect(html, css);
+
+ test('critical', () => expect(prettyPrint(critical)).toMatchSnapshot());
+ test('other', () => expect(prettyPrint(other)).toMatchSnapshot());
+});
+
+// there was a bug when the whole atrule was included for each child rule
+describe('include atrule once', () => {
+ const css = dedent`
+ @media screen {
+ body {
+ font-size: 10px;
+ }
+ h1 {
+ font-size: 20px;
+ }
+ .class {
+ font-size: 15px;
+ }
+ }
+ `;
+ const { critical, other } = collect(html, css);
+
+ test('critical', () => expect(prettyPrint(critical)).toMatchSnapshot());
+ test('other', () => expect(prettyPrint(other)).toMatchSnapshot());
+});
diff --git a/@linaria/packages/server/babel.config.js b/@linaria/packages/server/babel.config.js
new file mode 100644
index 0000000..c9ad680
--- /dev/null
+++ b/@linaria/packages/server/babel.config.js
@@ -0,0 +1,3 @@
+const config = require('../../babel.config');
+
+module.exports = config;
diff --git a/@linaria/packages/server/package.json b/@linaria/packages/server/package.json
new file mode 100644
index 0000000..162ea40
--- /dev/null
+++ b/@linaria/packages/server/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@linaria/server",
+ "version": "3.0.0-beta.3",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "Blazing fast zero-runtime CSS in JS library",
+ "main": "lib/index.js",
+ "module": "esm/index.js",
+ "types": "types",
+ "files": [
+ "types/",
+ "lib/",
+ "esm/"
+ ],
+ "license": "MIT",
+ "repository": "git@github.com:callstack/linaria.git",
+ "bugs": {
+ "url": "https://github.com/callstack/linaria/issues"
+ },
+ "homepage": "https://github.com/callstack/linaria#readme",
+ "keywords": [
+ "react",
+ "linaria",
+ "css",
+ "css-in-js",
+ "styled-components"
+ ],
+ "scripts": {
+ "build:lib": "cross-env NODE_ENV=legacy babel src --out-dir lib --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start",
+ "build:esm": "babel src --out-dir esm --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start",
+ "build": "yarn build:lib && yarn build:esm",
+ "build:declarations": "tsc --emitDeclarationOnly --outDir types",
+ "prepare": "yarn build && yarn build:declarations",
+ "typecheck": "tsc --noEmit --composite false",
+ "watch": "yarn build --watch"
+ },
+ "devDependencies": {
+ "@types/dedent": "^0.7.0",
+ "dedent": "^0.7.0",
+ "prettier": "^2.0.5"
+ },
+ "dependencies": {
+ "postcss": "^7.0.14"
+ }
+}
diff --git a/@linaria/packages/server/src/collect.ts b/@linaria/packages/server/src/collect.ts
new file mode 100644
index 0000000..5d2b197
--- /dev/null
+++ b/@linaria/packages/server/src/collect.ts
@@ -0,0 +1,115 @@
+/**
+ * This utility extracts critical CSS from given HTML and CSS file to be used in SSR environments
+ */
+
+import type { AtRule, ChildNode } from 'postcss';
+import postcss from 'postcss';
+
+type CollectResult = {
+ critical: string;
+ other: string;
+};
+
+export default function collect(html: string, css: string): CollectResult {
+ const animations = new Set();
+ const other = postcss.root();
+ const critical = postcss.root();
+ const stylesheet = postcss.parse(css);
+ const htmlClassesRegExp = extractClassesFromHtml(html);
+
+ const isCritical = (rule: ChildNode) => {
+ // Only check class names selectors
+ if ('selector' in rule && rule.selector.startsWith('.')) {
+ return Boolean(rule.selector.match(htmlClassesRegExp));
+ }
+
+ return true;
+ };
+
+ const handleAtRule = (rule: AtRule) => {
+ let addedToCritical = false;
+
+ rule.each((childRule) => {
+ if (isCritical(childRule) && !addedToCritical) {
+ critical.append(rule.clone());
+ addedToCritical = true;
+ }
+ });
+
+ if (rule.name === 'keyframes') {
+ return;
+ }
+
+ if (addedToCritical) {
+ rule.remove();
+ } else {
+ other.append(rule);
+ }
+ };
+
+ stylesheet.walkAtRules('font-face', (rule) => {
+ /**
+ * @font-face rules may be defined also in CSS conditional groups (eg. @media)
+ * we want only handle those from top-level, rest will be handled in stylesheet.walkRules
+ */
+ if (rule.parent.type === 'root') {
+ critical.append(rule);
+ }
+ });
+
+ const walkedAtRules = new Set();
+
+ stylesheet.walkRules((rule) => {
+ if ('name' in rule.parent && rule.parent.name === 'keyframes') {
+ return;
+ }
+
+ if (rule.parent.type === 'atrule') {
+ if (!walkedAtRules.has(rule.parent)) {
+ handleAtRule(rule.parent);
+ walkedAtRules.add(rule.parent);
+ }
+ return;
+ }
+
+ if (isCritical(rule)) {
+ critical.append(rule);
+ } else {
+ other.append(rule);
+ }
+ });
+
+ critical.walkDecls(/animation/, (decl) => {
+ animations.add(decl.value.split(' ')[0]);
+ });
+
+ stylesheet.walkAtRules('keyframes', (rule) => {
+ if (animations.has(rule.params)) {
+ critical.append(rule);
+ }
+ });
+
+ return {
+ critical: critical.toString(),
+ other: other.toString(),
+ };
+}
+
+const extractClassesFromHtml = (html: string): RegExp => {
+ const htmlClasses: string[] = [];
+ const regex = /\s+class="([^"]*)"/gm;
+ let match = regex.exec(html);
+
+ while (match !== null) {
+ match[1].split(' ').forEach((className) => {
+ className = className.replace(
+ /\\|\^|\$|\{|\}|\[|\]|\(|\)|\.|\*|\+|\?|\|/g,
+ '\\$&'
+ );
+ htmlClasses.push(className);
+ });
+ match = regex.exec(html);
+ }
+
+ return new RegExp(htmlClasses.join('|'), 'gm');
+};
diff --git a/@linaria/packages/server/src/index.ts b/@linaria/packages/server/src/index.ts
new file mode 100644
index 0000000..0d86f03
--- /dev/null
+++ b/@linaria/packages/server/src/index.ts
@@ -0,0 +1 @@
+export { default as collect } from './collect';
diff --git a/@linaria/packages/server/tsconfig.json b/@linaria/packages/server/tsconfig.json
new file mode 100644
index 0000000..3a01f90
--- /dev/null
+++ b/@linaria/packages/server/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "paths": {},
+ "rootDir": "src/"
+ }
+}