diff --git a/packages/core/src/layer.ts b/packages/core/src/layer.ts index f9904e12f..dcfb83fd4 100644 --- a/packages/core/src/layer.ts +++ b/packages/core/src/layer.ts @@ -81,17 +81,17 @@ export default class Layer { } } } - - if (this.native && 'nodes' in rule) { - const firstNode = rule.nodes[0] - if (firstNode?.native) { - const foundIndex = findNativeCSSRuleIndex(this.native.cssRules, firstNode.native) - if (foundIndex !== -1) { - rule.nodes.forEach(() => this.native?.deleteRule(foundIndex)) + if (this.native?.cssRules && 'nodes' in rule) { + for (const node of rule.nodes) { + if (node.native) { + const foundIndex = findNativeCSSRuleIndex(this.native.cssRules, node.native) + if (foundIndex !== -1) { + // todo: Firefox throw "Uncaught NS_ERROR_FAILURE". Reproduce: Add '@fade|1s @fade|2s' and remove '@fade|1s @fade|2s' + this.native.deleteRule(foundIndex) + } } } } - this.rules.splice(this.rules.indexOf(rule), 1) return rule } diff --git a/packages/runtime/e2e/keyframes.test.ts b/packages/runtime/e2e/keyframes.test.ts index 7714578a8..3254f6aff 100644 --- a/packages/runtime/e2e/keyframes.test.ts +++ b/packages/runtime/e2e/keyframes.test.ts @@ -11,7 +11,6 @@ test('expects the animation output', async ({ page }) => { document.body.append(p) }) expect(await page.evaluate(() => globalThis.runtimeCSS.text)).toContain('.\\@fade\\|1s{animation:fade 1s}') - await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.add( @@ -27,6 +26,18 @@ test('expects the animation output', async ({ page }) => { '{@zoom|1s;f:16}' ) }) + expect(await page.evaluate(() => globalThis.runtimeCSS.animationsNonLayer.usages)).toEqual({ + fade: 1, + flash: 1, + float: 1, + heart: 1, + jump: 1, + ping: 1, + pulse: 1, + rotate: 1, + shake: 1, + zoom: 2 + }) const cssText = await page.evaluate(() => globalThis.runtimeCSS.text) expect(cssText).toContain('@keyframes fade{0%{opacity:0}to{opacity:1}}') expect(cssText).toContain('@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}') @@ -42,66 +53,57 @@ test('expects the animation output', async ({ page }) => { const p = document.getElementById('mp') p?.classList.remove('@fade|1s') }) - const updatedCssText1 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText1).not.toContain('@keyframes fade{0%{opacity:0}to{opacity:1}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes fade{0%{opacity:0}to{opacity:1}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@flash|1s') }) - const updatedCssText2 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText2).not.toContain('@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@float|1s') }) - const updatedCssText3 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText3).not.toContain('@keyframes float{0%{transform:none}50%{transform:translateY(-1.25rem)}to{transform:none}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes float{0%{transform:none}50%{transform:translateY(-1.25rem)}to{transform:none}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@heart|1s') }) - const updatedCssText4 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText4).not.toContain('@keyframes heart{0%{transform:scale(1)}14%{transform:scale(1.3)}28%{transform:scale(1)}42%{transform:scale(1.3)}70%{transform:scale(1)}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes heart{0%{transform:scale(1)}14%{transform:scale(1.3)}28%{transform:scale(1)}42%{transform:scale(1.3)}70%{transform:scale(1)}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@jump|1s') }) - const updatedCssText5 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText5).not.toContain('@keyframes jump{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:translateY(0);animation-timing-function:cubic-bezier(0,0,.2,1)}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes jump{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:translateY(0);animation-timing-function:cubic-bezier(0,0,.2,1)}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@ping|1s') }) - const updatedCssText6 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText6).not.toContain('@keyframes ping{75%,to{transform:scale(2);opacity:0}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes ping{75%,to{transform:scale(2);opacity:0}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@pulse|1s') }) - const updatedCssText7 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText7).not.toContain('@keyframes pulse{0%{transform:none}50%{transform:scale(1.05)}to{transform:none}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes pulse{0%{transform:none}50%{transform:scale(1.05)}to{transform:none}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@rotate|1s') }) - const updatedCssText8 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText8).not.toContain('@keyframes rotate{0%{transform:rotate(-360deg)}to{transform:none}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes rotate{0%{transform:rotate(-360deg)}to{transform:none}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@shake|1s') }) - const updatedCssText9 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText9).not.toContain('@keyframes shake{0%{transform:none}6.5%{transform:translateX(-6px) rotateY(-9deg)}18.5%{transform:translateX(5px) rotateY(7deg)}31.5%{transform:translateX(-3px) rotateY(-5deg)}43.5%{transform:translateX(2px) rotateY(3deg)}50%{transform:none}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes shake{0%{transform:none}6.5%{transform:translateX(-6px) rotateY(-9deg)}18.5%{transform:translateX(5px) rotateY(7deg)}31.5%{transform:translateX(-3px) rotateY(-5deg)}43.5%{transform:translateX(2px) rotateY(3deg)}50%{transform:none}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('@zoom|1s') }) - const updatedCssText10 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText10).toContain('@keyframes zoom{0%{transform:scale(0)}to{transform:none}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.animationsNonLayer.usages)).toEqual({ zoom: 1 }) + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).toContain('@keyframes zoom{0%{transform:scale(0)}to{transform:none}}') await page.evaluate(() => { const p = document.getElementById('mp') p?.classList.remove('{@zoom|1s;f:16}') }) - const updatedCssText11 = await page.evaluate(() => globalThis.runtimeCSS.text) - expect(updatedCssText11).not.toContain('@keyframes zoom{0%{transform:scale(0)}to{transform:none}}') + expect(await page.evaluate(() => globalThis.runtimeCSS.animationsNonLayer.usages)).toEqual({}) + expect(await page.evaluate(() => globalThis.runtimeCSS.text)).not.toContain('@keyframes zoom{0%{transform:scale(0)}to{transform:none}}') }) diff --git a/packages/runtime/playground/index.html b/packages/runtime/playground/index.html index d02f82c13..90347bb34 100644 --- a/packages/runtime/playground/index.html +++ b/packages/runtime/playground/index.html @@ -1,335 +1,14 @@ -Introduction to Master CSS
Getting Started

Introduction to Master CSS

The CSS language and framework for rapidly building modern and high-performance websites.

What is Master CSS?

-

A CSS language

-

Master CSS is a markup-driven CSS language with smart rules allowing you to write familiar CSS using concise syntax within HTML/JSX.

-

Under the hood, Master CSS automatically adds vendor prefixes, smartly ordering CSS rules, and more. This allows you to focus on building your website rather than spending a lot of effort writing and maintaining CSS rules.

-

Hello World

-
<h1 class="fg:indigo fg:red:hover font:32 font:40@sm font:heavy m:10x text:center">
Hello World
</h1>
-

Declaring styles through the class attribute, attaching selector states, and even base them on applying conditionally. There's no need to write any inline style sheets, including creating external CSS files; just add syntax classes in your HTML.

-

You can write atomic classes text:center or abstract styles btn based on the characteristics of your project and team.

-

A CSS framework

-

Master CSS is a standalone CSS framework that provides various packages and solutions to expedite the development, including integrations for popular JavaScript frameworks, code linting for unifying team style, language service to enhance the developer experience, configuration API for design systems, and more.

-
View all official packages
PackageDescription
Main
@master/cssThe CSS language and framework for rapidly building modern and high-performance websites
@master/css-runtimeRun Master CSS right in the browser
@master/css-serverRender Master CSS on the server side
@master/css-extractorMaster CSS static extractor for various raw text
@master/css-validatorValidate Master CSS syntax
@master/css-cliMaster CSS CLI
@master/create-cssInitialize a new Master CSS project
Integrations
@master/css.reactIntegrate Master CSS in React way
@master/css.svelteIntegrate Master CSS in Svelte way
@master/css.vueIntegrate Master CSS in Vue way
@master/css.nuxtIntegrate Master CSS Progressive Rendering in Nuxt way
@master/css-extractor.viteIntegrate Master CSS Static Extraction in Vite way
@master/css-extractor.webpackIntegrate Master CSS Static Extraction in Webpack way
Developer Tools
@master/eslint-config-cssMaster CSS ESLint Configuration
@master/eslint-plugin-cssMaster CSS ESLint Plugin
@master/css-language-serverMaster CSS Language Server
@master/css-language-serviceMaster CSS Language Service
master-css-vscodeMaster CSS for Visual Studio Code
Other Solutions
@master/colorsA crafted color system for beautiful user interfaces
@master/normal.cssNormalize browser's styles ~600B
theme-modeA lightweight utility for switching CSS theme modes
class-variantCreate reusable and extensible style class variants
-
-

Why Master CSS?

-

The reasons why to use Master CSS with an overview of features, syntax benefits, benchmarks, and a comprehensive comparison:

-

Minimal CSS Output

-

This is a real-world benchmark of critical CSS on page load for well-known official documentation websites.

-
Master CSS0 kB
( 21.1 kB + Font 7.1 kB + Normal 1.7 kB )
React0 kB
React, 3.6x larger
Vue.js0 kB
Vue.js, 4.0x larger
Angular0 kB
Angular, 4.2x larger
Material UI0 kB
Material UI, 6.0x larger
Next.js0 kB
Next.js, 6.1x larger
Bootstrap0 kB
Bootstrap, 9.8x larger
Tailwind CSS0 kB
Tailwind CSS, 10.7x larger
The results are the size sum of internal CSS and external CSS, including font CSS
-

According to HTTP Archive CSS Bytes, the sum of transfer size kilobytes of all external stylesheets requested by the page is ~82 kB, excluding internal and inline styles.

-

Master CSS only injects the CSS required by the page, so the official documentation site is super tiny, about ~6 kB per page (brotli).

+ +

Hello World

+ + -

Master CSS provides three rendering modes, and you can choose according to project scale and application scenarios to meet your business needs.

-

One of the most unique, the progressive rendering works by pre-rendering the critical CSS based on the element's class names and deferring the loading of the runtime engine for any future dynamic class names.

-
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Generate only the critical CSS required for the page. -->
<style id="master">
.block {
display: block
}
</style>
<script src="https://cdn.master.co/css-runtime@rc" defer></script>
</head>
<body>
<h1 class="block">Hello World</h1>
</body>
</html>
-

This ensures the page generates the ultra-lightweight CSS and the fastest first-page render, unlike traditionally bundling styles across the entire application.

-

Initially, Master CSS was intended to create a CSS engine for production browser runtimes, so we concentrated on performance such as syntax parsing, rule generation, cache sharing, and even browser rendering.

-

Master CSS doesn't use heavy-duty build tools like PostCSS, Autoprefixer, or any minifiers, as these rules are generated with optimization.

-

Custom Design Systems

-

Master CSS provides rich configuration to help you build a consistent, easy-to-manage, and scalable design system. We've built a series of neutral design tokens and created a universal Master Design System, which makes your design system on top of or use it as a quick starting point.

-
VariableDescription
ColorsCustomizing color variables or starting with the crafted palette.
Primitive ColorsCustomizing primitive color variables or starting with the official design system.
Frame ColorsCustomizing frame color variables or starting with the official design system.
Text ColorsCustomizing text color variables or starting with the official design system.
FontsCustomizing fonts for your design system.
ScreensCustomizing screen sizes and breakpoints for your design system.
Spacing and SizingApply consistent sizes across your application.
-

This is just one part; more design variables and tokens are scattered throughout the syntax documentation.

-

Innovative Syntax Highlighting

-

Master CSS is the first language to highlight class names fully, making identifying their properties, appearance, and state faster.

-

Do you find writing utility classes sometimes seem lengthy or hard to read?

-
<div class="sm:hover:bg-[#ceb195] …">
-

Light up your class names in markup from now on!

-
<div class="bg:
#ceb195
:hover@sm
"
>
-

This feature is included in the Master CSS Language Service.

-

Reliable Code Linting

-

Have you ever felt anxious about adding styles to class attributes? Master CSS was the first to introduce syntax validation of classes in template markup, which helps you find errors early before building and dramatically improves the developer experience.

-Syntax error checks -

Invalid value for `font` property.

-
<div class="font:"></div>
-

This is just a piece of cake; we also allow you to enforce consistent class ordering, legitimate class checks, class collision checks, and checks for JavaScript utilities like clsx().

-

To learn more, check out the Code Linting documentation.

-

Comparisons

-

Here’s a comprehensive comparison of Master CSS, Tailwind CSS and Bootstrap:

-
Master CSSTailwind CSSBootstrap
Master CSSTailwind CSSBootstrap
Language--
Framework
Rendering
Runtime RenderingDev JIT
Server-side Rendering
CSS Hydration
Static Extraction
Preprocessors
PostCSSNoYesYes
AutoprefixerNoYesYes
CSS minifiersNoYesYes
Resource Size
Critical CSS for initial paintMinimalLargest~233 KB
Source that generates CSSPageAll files-
Generate redundant CSS rulesNoYesYes
Render-blocking CSS resourcesInternalExternalExternal
Programming
The class names from server-
The class names from static filesUnreliable-
The ability to customize tokens
Breakpoints and Screens1155
Colors~222~244~100
Variables with Modes
Selectors
Abstract Styles@applyPredefined
Language Service
Code-completion
Syntax highlighting for class names
CSS generate preview
Code Linting for Markup
Consistent class order
Syntax error checks
Disallow unknown classes
Class collision detection
-
-
Star us on GitHub ↗
Give more incentives for open source contributions: just star our repository.
Overview
Get started with Master CSS

This section walks you through learning and starting to use Master CSS.

Getting Started
Installing Master CSS

Master CSS allows you to install from package managers, copy-paste CDNs, and even integrate with frameworks.

© Aoyue Design LLC.
\ No newline at end of file + \ No newline at end of file diff --git a/packages/runtime/playground/src/main.ts b/packages/runtime/playground/src/main.ts index d147fc0c1..9311df090 100644 --- a/packages/runtime/playground/src/main.ts +++ b/packages/runtime/playground/src/main.ts @@ -1 +1,8 @@ -import '../../src/global.min' \ No newline at end of file +import '../../src/global.min' + +window.addEventListener('DOMContentLoaded', () => { + document.querySelector('h1')?.classList.add('@fade|1s', '@fade|2s') + setTimeout(() => { + document.querySelector('h1')?.classList.remove('@fade|1s', '@fade|2s') + }) +}) \ No newline at end of file diff --git a/packages/runtime/playwright.config.ts b/packages/runtime/playwright.config.ts index 43adbca06..9796cef26 100644 --- a/packages/runtime/playwright.config.ts +++ b/packages/runtime/playwright.config.ts @@ -37,10 +37,11 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, + // todo: Firefox throw "Uncaught NS_ERROR_FAILURE". Reproduce: Add '@fade|1s @fade|2s' and remove '@fade|1s @fade|2s' + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, diff --git a/site/package.json b/site/package.json index 15624945f..1d2b22ea4 100644 --- a/site/package.json +++ b/site/package.json @@ -5,7 +5,7 @@ "scripts": { "prepare-app": "tsx --env-file=.env ../internal/scripts/prepare-app", "dev": "pnpm prepare-app && next dev", - "build": "cross-env NODE_OPTIONS=--max-old-space-size=16384 pnpm prepare && cd ../ && pnpm build && cd site && next build && node ../packages/cli/dist/bin/index.mjs render \".next/**/*.html\"", + "build": "cross-env NODE_OPTIONS=--max-old-space-size=16384 pnpm prepare-app && cd ../ && pnpm build && cd site && next build && node ../packages/cli/dist/bin/index.mjs render \".next/**/*.html\"", "start": "next start", "lint": "next lint", "type-check": "tsc --noEmit"