diff --git a/packages/browser-vm/__tests__/__snapshots__/dynamic-style.spec.ts.snap b/packages/browser-vm/__tests__/__snapshots__/dynamic-style.spec.ts.snap
new file mode 100644
index 000000000..6f57f506a
--- /dev/null
+++ b/packages/browser-vm/__tests__/__snapshots__/dynamic-style.spec.ts.snap
@@ -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;}",
+ ],
+]
+`;
diff --git a/packages/browser-vm/__tests__/dom-side-effect.spec.ts b/packages/browser-vm/__tests__/dom-side-effect.spec.ts
index 1dd10fbe0..750365e9b 100644
--- a/packages/browser-vm/__tests__/dom-side-effect.spec.ts
+++ b/packages/browser-vm/__tests__/dom-side-effect.spec.ts
@@ -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 的相关方法进行测试
diff --git a/packages/browser-vm/__tests__/dom.spec.ts b/packages/browser-vm/__tests__/dom.spec.ts
index 83af3766e..f750581d8 100644
--- a/packages/browser-vm/__tests__/dom.spec.ts
+++ b/packages/browser-vm/__tests__/dom.spec.ts
@@ -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 的相关方法进行测试
@@ -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');
- });
});
diff --git a/packages/browser-vm/__tests__/dynamic-style.spec.ts b/packages/browser-vm/__tests__/dynamic-style.spec.ts
new file mode 100644
index 000000000..9a4fab226
--- /dev/null
+++ b/packages/browser-vm/__tests__/dynamic-style.spec.ts
@@ -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 = `
`;
+ 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');
+ });
+});
diff --git a/packages/browser-vm/src/dynamicNode/index.ts b/packages/browser-vm/src/dynamicNode/index.ts
index f9885f469..8569635ee 100644
--- a/packages/browser-vm/src/dynamicNode/index.ts
+++ b/packages/browser-vm/src/dynamicNode/index.ts
@@ -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',
@@ -137,31 +137,29 @@ export function makeElInjector(sandboxConfig: SandboxOptions) {
injectHandlerParams();
}
-export function recordStyledComponentCSSRules(
- dynamicStyleSheetElementSet: Set,
- styledComponentCSSRulesMap: WeakMap,
-) {
- dynamicStyleSheetElementSet.forEach((styleElement) => {
- if (isStyledComponentsLike(styleElement) && styleElement.sheet) {
- styledComponentCSSRulesMap.set(styleElement, styleElement.sheet.cssRules);
- }
- });
-}
-
export function rebuildCSSRules(
dynamicStyleSheetElementSet: Set,
- styledComponentCSSRulesMap: WeakMap,
+ 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);
+ }
}
}
});
diff --git a/packages/browser-vm/src/dynamicNode/processor.ts b/packages/browser-vm/src/dynamicNode/processor.ts
index dd2154d97..e63bcd999 100644
--- a/packages/browser-vm/src/dynamicNode/processor.ts
+++ b/packages/browser-vm/src/dynamicNode/processor.ts
@@ -15,7 +15,8 @@ import {
__REMOVE_NODE__,
} from '@garfish/utils';
import { Sandbox } from '../sandbox';
-import { rootElm, isStyledComponentsLike, LockQueue } from '../utils';
+import { rootElm, LockQueue } from '../utils';
+import { StyledComponentCSSRulesData } from '../types';
const isInsertMethod = makeMap(['insertBefore', 'insertAdjacentElement']);
@@ -252,26 +253,34 @@ export class DynamicNodeProcessor {
};
const mutator = new MutationObserver((mutations) => {
- for (const { type, target, addedNodes } of mutations) {
+ for (const { type, addedNodes } of mutations) {
if (type === 'childList') {
- const el = target as HTMLStyleElement;
- if (isStyledComponentsLike(el) && el.sheet) {
- const originAddRule = el.sheet.insertRule;
- el.sheet.insertRule = function () {
- arguments[0] = modifyStyleCode(arguments[0]);
- return originAddRule.apply(this, arguments);
- };
- } else {
- if (addedNodes[0]?.textContent) {
- addedNodes[0].textContent = modifyStyleCode(
- addedNodes[0].textContent,
- );
- }
+ if (addedNodes[0]?.textContent) {
+ addedNodes[0].textContent = modifyStyleCode(
+ addedNodes[0].textContent,
+ );
}
}
}
});
mutator.observe(this.el, { childList: true });
+
+ // Handle `sheet.cssRules` (styled-components)
+ let fakeSheet: any = null;
+ Reflect.defineProperty(this.el, 'sheet', {
+ get: () => {
+ // styled-components only get the `sheet` once, and keep the first
+ // instance in their state. But the `sheet` will be actually replaced
+ // with another instance after remount.
+ // To make insertRule() after remount possible, we return a fake sheet
+ // here and passthrough operations to the latest real `sheet`.
+ if (!fakeSheet) {
+ fakeSheet = this.createFakeSheet(modifyStyleCode);
+ }
+ return fakeSheet;
+ },
+ configurable: true,
+ });
}
private findParentNodeInApp(parentNode: Element, defaultInsert?: string) {
@@ -432,7 +441,42 @@ export class DynamicNodeProcessor {
return this.nativeRemove.call(parentNode, this.el);
}
}
-
return originProcess();
}
+
+ private getRealSheet() {
+ return Reflect.get(HTMLStyleElement.prototype, 'sheet', this.el);
+ }
+
+ private createFakeSheet(
+ styleTransformer: (css: string | null) => string | null,
+ ) {
+ const processor = this;
+ const rulesData: StyledComponentCSSRulesData = [];
+ this.sandbox.styledComponentCSSRulesMap.set(this.el, rulesData);
+
+ const fakeSheet = {
+ get cssRules() {
+ const realSheet = processor.getRealSheet();
+ return realSheet?.cssRules ?? [];
+ },
+ insertRule(rule: string, index?: number) {
+ const realSheet = processor.getRealSheet();
+ const transformed = styleTransformer(rule)!;
+ if (realSheet) {
+ realSheet.insertRule(transformed, index);
+ }
+ rulesData.splice(index || 0, 0, transformed);
+ return index || 0;
+ },
+ deleteRule(index: number) {
+ const realSheet = processor.getRealSheet();
+ if (realSheet) {
+ realSheet.deleteRule(index);
+ }
+ rulesData.splice(index, 1);
+ },
+ };
+ return fakeSheet;
+ }
}
diff --git a/packages/browser-vm/src/pluginify.ts b/packages/browser-vm/src/pluginify.ts
index 804ee4be3..e2a235702 100644
--- a/packages/browser-vm/src/pluginify.ts
+++ b/packages/browser-vm/src/pluginify.ts
@@ -2,7 +2,7 @@ import { interfaces } from '@garfish/core';
import { warn, isPlainObject } from '@garfish/utils';
import { Module } from './types';
import { Sandbox } from './sandbox';
-import { recordStyledComponentCSSRules, rebuildCSSRules } from './dynamicNode';
+import { rebuildCSSRules } from './dynamicNode';
declare module '@garfish/core' {
export default interface Garfish {
@@ -147,15 +147,6 @@ function createOptions(Garfish: interfaces.Garfish) {
);
},
- beforeUnmount(appInfo, appInstance) {
- if (appInstance.vmSandbox) {
- recordStyledComponentCSSRules(
- appInstance.vmSandbox.dynamicStyleSheetElementSet,
- appInstance.vmSandbox.styledComponentCSSRulesMap,
- );
- }
- },
-
// If the app is uninstalled, the sandbox needs to clear all effects and then reset
afterUnmount(appInfo, appInstance, isCacheMode) {
// The caching pattern to retain the same context
diff --git a/packages/browser-vm/src/sandbox.ts b/packages/browser-vm/src/sandbox.ts
index a32806f52..2219b4683 100644
--- a/packages/browser-vm/src/sandbox.ts
+++ b/packages/browser-vm/src/sandbox.ts
@@ -23,7 +23,12 @@ import { makeElInjector } from './dynamicNode';
import { sandboxLifecycle } from './lifecycle';
import { optimizeMethods, createFakeObject, sandboxMap } from './utils';
import { __garfishGlobal__, GARFISH_OPTIMIZE_NAME } from './symbolTypes';
-import { Module, SandboxOptions, ReplaceGlobalVariables } from './types';
+import {
+ Module,
+ SandboxOptions,
+ ReplaceGlobalVariables,
+ StyledComponentCSSRulesData,
+} from './types';
import {
createHas,
createGetter,
@@ -76,7 +81,7 @@ export class Sandbox {
public dynamicStyleSheetElementSet = new Set();
public styledComponentCSSRulesMap = new WeakMap<
HTMLStyleElement,
- CSSRuleList
+ StyledComponentCSSRulesData
>();
private optimizeCode = ''; // To optimize the with statement
diff --git a/packages/browser-vm/src/types.ts b/packages/browser-vm/src/types.ts
index e5cd8d201..c8747d6a8 100644
--- a/packages/browser-vm/src/types.ts
+++ b/packages/browser-vm/src/types.ts
@@ -38,3 +38,5 @@ export interface SandboxOptions {
protectVariable?: () => Array;
insulationVariable?: () => Array;
}
+
+export type StyledComponentCSSRulesData = Array;
diff --git a/packages/browser-vm/src/utils.ts b/packages/browser-vm/src/utils.ts
index a8da52519..439aac546 100644
--- a/packages/browser-vm/src/utils.ts
+++ b/packages/browser-vm/src/utils.ts
@@ -166,8 +166,7 @@ export function isStyledComponentsLike(element: HTMLStyleElement) {
// A styled-components liked element has no textContent but keep the rules in its sheet.cssRules.
return (
element instanceof HTMLStyleElement &&
- !element.textContent &&
- element.sheet?.cssRules.length
+ !element.textContent
);
}