Skip to content

Commit

Permalink
fix: styled-components / insertRule (#621)
Browse files Browse the repository at this point in the history
* fix: reimplement insertRule hook

* test: add dynamic style node tests

* fix: styled-components on remount

* fix: early record cssRules for style rebuild

* test: update style unit tests

* fix: remove unused imports

* fix: styled-components adding rules after remount

* test: adding rules after remount

* fix: recording of css rules
  • Loading branch information
lideming authored Jun 7, 2023
1 parent e6189a1 commit 2a14077
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Sandbox: dynamic style append text node 1`] = `
Array [
".app .a { color: black; }.app .b { color: white; }",
Array [
".app .a {color: black;}",
".app .b {color: white;}",
],
]
`;

exports[`Sandbox: dynamic style insertRule 1`] = `
Array [
".app .a {color: black;}",
".app .b {color: white;}",
".app .c {color: white;}",
]
`;

exports[`Sandbox: dynamic style set textContent 1`] = `
Array [
".app .a { color: black; }",
Array [
".app .a {color: black;}",
],
]
`;
4 changes: 0 additions & 4 deletions packages/browser-vm/__tests__/dom-side-effect.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Sandbox } from '../src/sandbox';
import assert from 'assert';
import { sandboxMap } from '../src/utils';
import {
recordStyledComponentCSSRules,
rebuildCSSRules,
} from '../src/dynamicNode';

// Garfish 使用 Proxy 对 dom 进行了劫持, 同时对调用 dom 的函数做了劫持, 修正 dom 节点的类型
// 对调用 dom 的相关方法进行测试
Expand Down
49 changes: 0 additions & 49 deletions packages/browser-vm/__tests__/dom.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { Sandbox } from '../src/sandbox';
import { sandboxMap } from '../src/utils';
import {
recordStyledComponentCSSRules,
rebuildCSSRules,
} from '../src/dynamicNode';

// Garfish 使用 Proxy 对 dom 进行了劫持, 同时对调用 dom 的函数做了劫持, 修正 dom 节点的类型
// 对调用 dom 的相关方法进行测试
Expand Down Expand Up @@ -160,49 +156,4 @@ describe('Sandbox:Dom & Bom', () => {
{ next },
);
});

const createStyleComponentElementWithRecord = () => {
const styleComponentElement = document.createElement('style');
document.head.appendChild(styleComponentElement);
const cssRuleExample1 = '.test1 { color: red }';
const cssRuleExample2 = '.test2 { color: red }';
styleComponentElement.sheet?.insertRule(`${cssRuleExample1}`);
styleComponentElement.sheet?.insertRule(`${cssRuleExample2}`);
sandbox.dynamicStyleSheetElementSet.add(styleComponentElement);

recordStyledComponentCSSRules(
sandbox.dynamicStyleSheetElementSet,
sandbox.styledComponentCSSRulesMap,
);
return styleComponentElement;
};

it('should record the css rules of styled-components correctly', () => {
const styleComponentElement = createStyleComponentElementWithRecord();
const cssRules = sandbox.styledComponentCSSRulesMap.get(
styleComponentElement,
);
expect(cssRules?.length).toEqual(2);
expect((cssRules?.[0] as CSSStyleRule).selectorText).toEqual('.test2');
expect((cssRules?.[1] as CSSStyleRule).selectorText).toEqual('.test1');
});

it('should rebuild the css rules of styled-components in the correct order', () => {
const styleComponentElement = createStyleComponentElementWithRecord();

Object.defineProperty(window.HTMLStyleElement.prototype, 'sheet', {
writable: true,
value: {},
});
// @ts-ignore
styleComponentElement.sheet = new CSSStyleSheet();
rebuildCSSRules(
sandbox.dynamicStyleSheetElementSet,
sandbox.styledComponentCSSRulesMap,
);
const cssRules = styleComponentElement.sheet.cssRules;
expect(cssRules.length).toEqual(2);
expect((cssRules?.[0] as CSSStyleRule).selectorText).toEqual('.test2');
expect((cssRules?.[1] as CSSStyleRule).selectorText).toEqual('.test1');
});
});
155 changes: 155 additions & 0 deletions packages/browser-vm/__tests__/dynamic-style.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { __MockHead__ } from '@garfish/utils';
import { Sandbox } from '../src/sandbox';
import { StyleManager } from '@garfish/loader';
import { rebuildCSSRules } from '../src/dynamicNode';

describe('Sandbox: dynamic style', () => {
let sandbox: Sandbox;

const go = (code: string | (() => void)) => {
if (typeof code === 'function') {
code = `(${code})();`;
}
return `
const sandbox = __debug_sandbox__;
const Sandbox = sandbox.constructor;
const nativeWindow = Sandbox.getNativeWindow();
${code}
`;
};

// Mock style transformer
const styleManagerProto = StyleManager.prototype;
const originalTransformCode = styleManagerProto.transformCode;
styleManagerProto.transformCode = function (code) {
return originalTransformCode('.app ' + code);
};

beforeEach(() => {
document.body.innerHTML = `<div id="root">123<div ${__MockHead__}></div></div>`;
sandbox = new Sandbox({
namespace: 'app',
el: () => document.querySelector('#root'),
baseUrl: 'http://test.app',
modules: [
() => ({
recover() {},
override: { go, jest, expect },
}),
],
});
});

it('set textContent', (done) => {
sandbox.execScript(
go(() => {
const style = document.createElement('style');
style.textContent = '.a { color: black; }';
document.head.appendChild(style);
expect([
style.textContent,
Array.from(style.sheet!.cssRules).map((x) => x.cssText),
]).toMatchSnapshot();
done();
}),
{ done },
);
});

it('append text node', (done) => {
sandbox.execScript(
go(() => {
const style = document.createElement('style');
style.textContent = '.a { color: black; }';
document.head.appendChild(style);
style.appendChild(document.createTextNode('.b { color: white; }'));
expect([
style.textContent,
Array.from(style.sheet!.cssRules).map((x) => x.cssText),
]).toMatchSnapshot();
done();
}),
{ done },
);
});

it('insertRule', (done) => {
sandbox.execScript(
go(() => {
const style = document.createElement('style');
document.head.appendChild(style);
style.sheet!.insertRule('.b { color: white; }');
style.sheet!.insertRule('.a { color: black; }');
style.sheet!.insertRule('.c { color: white; }', 2);
expect(
Array.from(style.sheet!.cssRules).map((x) => x.cssText),
).toMatchSnapshot();
done();
}),
{ done },
);
});

const createStyleComponentElementWithRecord = () => {
sandbox.execScript(
go(() => {
const styleComponentElement = document.createElement('style');
document.head.appendChild(styleComponentElement);
const cssRuleExample1 = '.test1 { color: red }';
const cssRuleExample2 = '.test2 { color: red }';
styleComponentElement.sheet!.insertRule(cssRuleExample1);
styleComponentElement.sheet!.insertRule(cssRuleExample2);
window._styleElement = styleComponentElement;
}),
);

const style = sandbox.global!._styleElement as HTMLStyleElement;
sandbox.dynamicStyleSheetElementSet.add(style);
return style;
};

it('should record the css rules of styled-components correctly', () => {
const styleComponentElement = createStyleComponentElementWithRecord();
const data = sandbox.styledComponentCSSRulesMap.get(styleComponentElement);
expect(data!.length).toEqual(2);
expect(data![0].split(' {')[0]).toEqual('.app .test2');
expect(data![1].split(' {')[0]).toEqual('.app .test1');
});

it('should rebuild the css rules of styled-components in the correct order', () => {
const styleComponentElement = createStyleComponentElementWithRecord();

const parent = styleComponentElement.parentElement;
styleComponentElement.remove();
parent!.appendChild(styleComponentElement);

rebuildCSSRules(
sandbox.dynamicStyleSheetElementSet,
sandbox.styledComponentCSSRulesMap,
);
const cssRules = styleComponentElement.sheet!.cssRules;
expect(cssRules.length).toEqual(2);
expect((cssRules![0] as CSSStyleRule).selectorText).toEqual('.app .test2');
expect((cssRules![1] as CSSStyleRule).selectorText).toEqual('.app .test1');
});

it('should be able to insertRule on old sheet after remount', () => {
const styleComponentElement = createStyleComponentElementWithRecord();
const oldSheet = styleComponentElement.sheet;

const parent = styleComponentElement.parentElement;
styleComponentElement.remove();
parent!.appendChild(styleComponentElement);

rebuildCSSRules(
sandbox.dynamicStyleSheetElementSet,
sandbox.styledComponentCSSRulesMap,
);
const cssRules = styleComponentElement.sheet!.cssRules;
expect(cssRules.length).toEqual(2);
expect((cssRules![0] as CSSStyleRule).selectorText).toEqual('.app .test2');
expect((cssRules![1] as CSSStyleRule).selectorText).toEqual('.app .test1');
oldSheet!.insertRule('.test3 {}', 2);
expect((cssRules![2] as CSSStyleRule).selectorText).toEqual('.app .test3');
});
});
42 changes: 20 additions & 22 deletions packages/browser-vm/src/dynamicNode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { __domWrapper__ } from '../symbolTypes';
import { injectHandlerParams } from './processParams';
import { DynamicNodeProcessor, rawElementMethods } from './processor';
import { isInIframe, sandboxMap, isStyledComponentsLike } from '../utils';
import { SandboxOptions } from '../types';
import { SandboxOptions, StyledComponentCSSRulesData } from '../types';

const mountElementMethods = [
'append',
Expand Down Expand Up @@ -137,31 +137,29 @@ export function makeElInjector(sandboxConfig: SandboxOptions) {
injectHandlerParams();
}

export function recordStyledComponentCSSRules(
dynamicStyleSheetElementSet: Set<HTMLStyleElement>,
styledComponentCSSRulesMap: WeakMap<HTMLStyleElement, CSSRuleList>,
) {
dynamicStyleSheetElementSet.forEach((styleElement) => {
if (isStyledComponentsLike(styleElement) && styleElement.sheet) {
styledComponentCSSRulesMap.set(styleElement, styleElement.sheet.cssRules);
}
});
}

export function rebuildCSSRules(
dynamicStyleSheetElementSet: Set<HTMLStyleElement>,
styledComponentCSSRulesMap: WeakMap<HTMLStyleElement, CSSRuleList>,
styledComponentCSSRulesMap: WeakMap<
HTMLStyleElement,
StyledComponentCSSRulesData
>,
) {
dynamicStyleSheetElementSet.forEach((styleElement) => {
const cssRules = styledComponentCSSRulesMap.get(styleElement);
if (cssRules && (isStyledComponentsLike(styleElement) || cssRules.length)) {
for (let i = 0; i < cssRules.length; i++) {
const cssRule = cssRules[i];
// re-insert rules for styled-components element
styleElement.sheet?.insertRule(
cssRule.cssText,
styleElement.sheet?.cssRules.length,
);
const rules = styledComponentCSSRulesMap.get(styleElement);

if (rules && (isStyledComponentsLike(styleElement) || rules.length)) {
const realSheet = Reflect.get(
HTMLStyleElement.prototype,
'sheet',
styleElement,
);
if (realSheet) {
for (let i = 0; i < rules.length; i++) {
const cssRule = rules[i];
// re-insert rules for styled-components element
// use realSheet to skip transforming
realSheet.insertRule(cssRule, i);
}
}
}
});
Expand Down
Loading

1 comment on commit 2a14077

@vercel
Copy link

@vercel vercel bot commented on 2a14077 Jun 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.