From a637112231ff64f11012e954635f206101a736b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 18 Mar 2021 14:40:30 +0800 Subject: [PATCH] feat: Support csp inject (#212) * feat: Support csp inject * test: Add test case --- src/Dom/dynamicCSS.ts | 41 +++++++++++++++++++++++ tests/dynamicCSS.test.tsx | 70 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 src/Dom/dynamicCSS.ts create mode 100644 tests/dynamicCSS.test.tsx diff --git a/src/Dom/dynamicCSS.ts b/src/Dom/dynamicCSS.ts new file mode 100644 index 00000000..a34da831 --- /dev/null +++ b/src/Dom/dynamicCSS.ts @@ -0,0 +1,41 @@ +import canUseDom from './canUseDom'; + +const MARK_KEY = `rc-util-key` as any; + +interface Options { + attachTo?: Element; + csp?: string; +} + +function getContainer(option: Options) { + return option.attachTo || document.body; +} + +export function injectCSS(css: string, option: Options = {}) { + if (!canUseDom()) { + return null; + } + + const styleNode = document.createElement('style'); + styleNode.nonce = option.csp; + styleNode.innerHTML = css; + + getContainer(option).appendChild(styleNode); + + return styleNode; +} + +export function updateCSS(css: string, key: string, option: Options = {}) { + const container = getContainer(option); + const existNode = [...container.children].find( + node => node.tagName === 'STYLE' && node[MARK_KEY] === key, + ); + + if (existNode) { + existNode.parentElement.removeChild(existNode); + } + + const newNode = injectCSS(css, option); + newNode[MARK_KEY] = key; + return newNode; +} diff --git a/tests/dynamicCSS.test.tsx b/tests/dynamicCSS.test.tsx new file mode 100644 index 00000000..33f171d6 --- /dev/null +++ b/tests/dynamicCSS.test.tsx @@ -0,0 +1,70 @@ +/* eslint-disable no-eval */ +import { injectCSS, updateCSS } from '../src/Dom/dynamicCSS'; + +const TEST_STYLE = '.bamboo { context: "light" }'; + +describe('dynamicCSS', () => { + describe('injectCSS', () => { + beforeEach(() => { + expect(document.querySelectorAll('style')).toHaveLength(0); + }); + + afterEach(() => { + const styles = document.querySelectorAll('style'); + styles.forEach(style => { + style.parentNode.removeChild(style); + }); + }); + + it('basic', () => { + const style = injectCSS(TEST_STYLE); + expect(document.body.contains(style)); + expect(document.body.querySelector('style').innerHTML).toEqual( + TEST_STYLE, + ); + }); + + it('with CSP', () => { + const style = injectCSS(TEST_STYLE, { csp: 'light' }); + expect(document.body.contains(style)); + expect(document.body.querySelector('style').innerHTML).toEqual( + TEST_STYLE, + ); + expect(document.body.querySelector('style').nonce).toEqual('light'); + }); + }); + + describe('updateCSS', () => { + beforeAll(() => { + updateCSS(TEST_STYLE, 'unique'); + }); + + afterAll(() => { + const styles = document.querySelectorAll('style'); + styles.forEach(style => { + style.parentNode.removeChild(style); + }); + }); + + it('replace', () => { + const REPLACE_STYLE = '.light { context: "bamboo" }'; + updateCSS(REPLACE_STYLE, 'unique'); + + expect(document.querySelectorAll('style')).toHaveLength(1); + expect(document.body.querySelector('style').innerHTML).toEqual( + REPLACE_STYLE, + ); + }); + + it('replace with CSP', () => { + const REPLACE_STYLE = '.bamboo { context: "little" }'; + updateCSS(REPLACE_STYLE, 'unique', { csp: 'only' }); + + expect(document.querySelectorAll('style')).toHaveLength(1); + expect(document.body.querySelector('style').innerHTML).toEqual( + REPLACE_STYLE, + ); + expect(document.body.querySelector('style').nonce).toEqual('only'); + }); + }); +});