diff options
author | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:46:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:48:30 -0300 |
commit | 38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch) | |
tree | 453dbf70000cc5e338b06201af1eaca8343f8f73 /@linaria/packages/server | |
parent | f26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff) | |
download | node-vendor-master.tar.gz node-vendor-master.tar.bz2 node-vendor-master.zip |
Diffstat (limited to '@linaria/packages/server')
-rw-r--r-- | @linaria/packages/server/CHANGELOG.md | 8 | ||||
-rw-r--r-- | @linaria/packages/server/README.md | 35 | ||||
-rw-r--r-- | @linaria/packages/server/__tests__/__snapshots__/collect.test.ts.snap | 300 | ||||
-rw-r--r-- | @linaria/packages/server/__tests__/collect.test.ts | 235 | ||||
-rw-r--r-- | @linaria/packages/server/babel.config.js | 3 | ||||
-rw-r--r-- | @linaria/packages/server/package.json | 46 | ||||
-rw-r--r-- | @linaria/packages/server/src/collect.ts | 115 | ||||
-rw-r--r-- | @linaria/packages/server/src/index.ts | 1 | ||||
-rw-r--r-- | @linaria/packages/server/tsconfig.json | 7 |
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/" + } +} |