feat: add new css parser - postcss (#1458)

* feat: add new css parser

* make selectors change

* selectors and tests

* media changes

* remove old css references

* better variable name

* use postcss and port tests

* fix media test

* inline plugins

* fix failing multiline selector

* correct test result

* move tests to correct file

* cleanup all tests

* remove unused css-tree

* update bundle

* cleanup dependencies

* revert config files to master

* remove d.ts files

* update snapshot

* reset rebuilt test

* apply fuzzy css matching

* remove extra test

* Fix imports

* Newer versions of nswapi break rrdom-nodejs tests.
Example:
 FAIL  test/document-nodejs.test.ts > RRDocument for nodejs environment > RRDocument API > querySelectorAll
TypeError: e[api] is not a function
 ❯ byTag ../../node_modules/nwsapi/src/nwsapi.js:390:37
 ❯ Array.<anonymous> ../../node_modules/nwsapi/src/nwsapi.js:327:113
 ❯ collect ../../node_modules/nwsapi/src/nwsapi.js:1578:32
 ❯ Object._querySelectorAll [as select] ../../node_modules/nwsapi/src/nwsapi.js:1533:36
 ❯ RRDocument.querySelectorAll src/document-nodejs.ts:96:24

* Migrate from jest to vitest

* Order of selectors has changed with postcss

* Remove unused eslint

---------

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
This commit is contained in:
David Newell
2026-04-01 12:00:00 +08:00
committed by GitHub
parent a0b3d054b7
commit 83e8dd8ab7
9 changed files with 3742 additions and 5764 deletions

View File

@@ -3,7 +3,7 @@
*/
import * as fs from 'fs';
import * as path from 'path';
import { describe, it, beforeEach, expect } from 'vitest';
import { describe, it, beforeEach, expect as _expect } from 'vitest';
import {
adaptCssForReplay,
buildNodeWithSN,
@@ -12,6 +12,30 @@ import {
import { NodeType } from '../src/types';
import { createMirror, Mirror } from '../src/utils';
const expect = _expect as unknown as {
<T = unknown>(actual: T): {
toMatchCss(expected: string): void;
} & ReturnType<typeof _expect>;
} & typeof _expect;
expect.extend({
toMatchCss: function (received: string, expected: string) {
const pass = normCss(received) === normCss(expected);
const message: () => string = () =>
pass
? ''
: `Received (${received}) is not the same as expected (${expected})`;
return {
message,
pass,
};
},
});
function normCss(cssText: string): string {
return cssText.replace(/[\s;]/g, '');
}
function getDuration(hrtime: [number, number]) {
const [seconds, nanoseconds] = hrtime;
return seconds * 1000 + nanoseconds / 1000000;
@@ -86,19 +110,19 @@ describe('rebuild', function () {
describe('add hover class to hover selector related rules', function () {
it('will do nothing to css text without :hover', () => {
const cssText = 'body { color: white }';
expect(adaptCssForReplay(cssText, cache)).toEqual(cssText);
expect(adaptCssForReplay(cssText, cache)).toMatchCss(cssText);
});
it('can add hover class to css text', () => {
const cssText = '.a:hover { color: white }';
expect(adaptCssForReplay(cssText, cache)).toEqual(
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
'.a:hover, .a.\\:hover { color: white }',
);
});
it('can correctly add hover when in middle of selector', () => {
const cssText = 'ul li a:hover img { color: white }';
expect(adaptCssForReplay(cssText, cache)).toEqual(
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
'ul li a:hover img, ul li a.\\:hover img { color: white }',
);
});
@@ -111,14 +135,15 @@ img,
ul li.specified c:hover img {
color: white
}`;
expect(adaptCssForReplay(cssText, cache)).toEqual(
`ul li.specified a:hover img, ul li.specified a.\\:hover img,
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
`ul li.specified a:hover img,
ul li.multiline
b:hover
img, ul li.multiline
b.\\:hover
img,
ul li.specified c:hover img, ul li.specified c.\\:hover img {
ul li.specified c:hover img,
ul li.specified a.\\:hover img,
ul li.multiline b.\\:hover img,
ul li.specified c.\\:hover img {
color: white
}`,
);
@@ -126,48 +151,48 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img {
it('can add hover class within media query', () => {
const cssText = '@media screen { .m:hover { color: white } }';
expect(adaptCssForReplay(cssText, cache)).toEqual(
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
'@media screen { .m:hover, .m.\\:hover { color: white } }',
);
});
it('can add hover class when there is multi selector', () => {
const cssText = '.a, .b:hover, .c { color: white }';
expect(adaptCssForReplay(cssText, cache)).toEqual(
'.a, .b:hover, .b.\\:hover, .c { color: white }',
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
'.a, .b:hover, .c, .b.\\:hover { color: white }',
);
});
it('can add hover class when there is a multi selector with the same prefix', () => {
const cssText = '.a:hover, .a:hover::after { color: white }';
expect(adaptCssForReplay(cssText, cache)).toEqual(
'.a:hover, .a.\\:hover, .a:hover::after, .a.\\:hover::after { color: white }',
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
'.a:hover, .a:hover::after, .a.\\:hover, .a.\\:hover::after { color: white }',
);
});
it('can add hover class when :hover is not the end of selector', () => {
const cssText = 'div:hover::after { color: white }';
expect(adaptCssForReplay(cssText, cache)).toEqual(
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
'div:hover::after, div.\\:hover::after { color: white }',
);
});
it('can add hover class when the selector has multi :hover', () => {
const cssText = 'a:hover b:hover { color: white }';
expect(adaptCssForReplay(cssText, cache)).toEqual(
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
'a:hover b:hover, a.\\:hover b.\\:hover { color: white }',
);
});
it('will ignore :hover in css value', () => {
const cssText = '.a::after { content: ":hover" }';
expect(adaptCssForReplay(cssText, cache)).toEqual(cssText);
expect(adaptCssForReplay(cssText, cache)).toMatchCss(cssText);
});
it('can adapt media rules to replay context', () => {
const cssText =
'@media only screen and (min-device-width : 1200px) { .a { width: 10px; }}';
expect(adaptCssForReplay(cssText, cache)).toEqual(
expect(adaptCssForReplay(cssText, cache)).toMatchCss(
'@media only screen and (min-width : 1200px) { .a { width: 10px; }}',
);
});
@@ -210,7 +235,7 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img {
// previously that part was being incorrectly consumed by the selector regex
const should_not_modify =
".tailwind :is(.before\\:content-\\[\\'\\'\\])::before { --tw-content: \":hover\"; content: var(--tw-content); }.tailwind :is(.\\[\\&\\>li\\]\\:before\\:content-\\[\\'-\\'\\] > li)::before { color: pink; }";
expect(adaptCssForReplay(should_not_modify, cache)).toEqual(
expect(adaptCssForReplay(should_not_modify, cache)).toMatchCss(
should_not_modify,
);
});
@@ -219,7 +244,7 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img {
// the ':hover' in the below is a decoy which is not part of the selector,
const should_not_modify =
'@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,700;1,400&display=:hover");';
expect(adaptCssForReplay(should_not_modify, cache)).toEqual(
expect(adaptCssForReplay(should_not_modify, cache)).toMatchCss(
should_not_modify,
);
});