From 5f7e640929e3f36fd8cecc03db49a90b176c9c39 Mon Sep 17 00:00:00 2001 From: Moritz Eck Date: Tue, 19 Nov 2024 00:53:43 +0100 Subject: [PATCH] feat(css): add CSP nonce to hono/css related style and script tags --- runtime-tests/deno-jsx/jsx.test.tsx | 23 +++++++++++++++ src/helper/css/common.case.test.tsx | 15 ++++++++++ src/helper/css/index.test.tsx | 14 ++++++++- src/helper/css/index.ts | 45 ++++++++++++++++++++++++----- src/jsx/dom/css.test.tsx | 12 ++++++++ src/jsx/dom/css.ts | 3 +- 6 files changed, 102 insertions(+), 10 deletions(-) diff --git a/runtime-tests/deno-jsx/jsx.test.tsx b/runtime-tests/deno-jsx/jsx.test.tsx index 4b58a34a..414869df 100644 --- a/runtime-tests/deno-jsx/jsx.test.tsx +++ b/runtime-tests/deno-jsx/jsx.test.tsx @@ -111,6 +111,29 @@ Deno.test('JSX: css', async () => { ) }) +Deno.test('JSX: css with CSP nonce', async () => { + const className = css` + color: red; + ` + const html = ( + + +
' + ) +}) + Deno.test('JSX: normalize key', async () => { const className =
const htmlFor =
diff --git a/src/helper/css/common.case.test.tsx b/src/helper/css/common.case.test.tsx index 74422944..112acd19 100644 --- a/src/helper/css/common.case.test.tsx +++ b/src/helper/css/common.case.test.tsx @@ -488,6 +488,21 @@ export const renderTest = ( '

Hello!

' ) }) + + it('Should render CSS styles with CSP nonce', async () => { + const headerClass = css` + background-color: blue; + ` + const template = ( + <> +

Hello!

' + ) + }) }) }) } diff --git a/src/helper/css/index.test.tsx b/src/helper/css/index.test.tsx index 679f99e1..3e84a952 100644 --- a/src/helper/css/index.test.tsx +++ b/src/helper/css/index.test.tsx @@ -1,8 +1,8 @@ /** @jsxImportSource ../../jsx */ import { Hono } from '../../' import { html } from '../../helper/html' -import { isValidElement } from '../../jsx' import type { JSXNode } from '../../jsx' +import { isValidElement } from '../../jsx' import { Suspense, renderToReadableStream } from '../../jsx/streaming' import type { HtmlEscapedString } from '../../utils/html' import { HtmlEscapedCallbackPhase, resolveCallback } from '../../utils/html' @@ -58,6 +58,18 @@ describe('CSS Helper', () => {

Hello!

` ) }) + + it('Should render CSS styles with `html` tag function and CSP nonce', async () => { + const headerClass = css` + background-color: blue; + ` + const template = html`${Style({ nonce: '1234' })} +

Hello!

` + expect(await toString(template)).toBe( + ` +

Hello!

` + ) + }) }) describe('cx()', () => { diff --git a/src/helper/css/index.ts b/src/helper/css/index.ts index f1aa02a8..417923af 100644 --- a/src/helper/css/index.ts +++ b/src/helper/css/index.ts @@ -50,7 +50,7 @@ interface ViewTransitionType { } interface StyleType { - (args?: { children?: Promise }): HtmlEscapedString + (args?: { children?: Promise; nonce?: string }): HtmlEscapedString } /** @@ -88,9 +88,12 @@ export const createCssContext = ({ id }: { id: Readonly }): DefaultConte return } - const appendStyleScript = `` + const styleNonce = (context as any)?.style?.nonce + + const appendStyleScript = `document.querySelector('#${id}').textContent+=${JSON.stringify(stylesStr)}` + if (buffer) { buffer[0] = `${appendStyleScript}${buffer[0]}` return @@ -156,10 +159,36 @@ export const createCssContext = ({ id }: { id: Readonly }): DefaultConte return newCssClassNameObject(viewTransitionCommon(strings as any, values)) }) as ViewTransitionType - const Style: StyleType = ({ children } = {}) => - children - ? raw(``) - : raw(``) + const Style: StyleType = ({ children, nonce } = {}) => { + const styleTag = children + ? raw( + `` + ) + : raw(``) + + ;(styleTag as any).nonce = nonce + + const storeNonce: HtmlEscapedCallback = ({ context }) => { + if (!nonce) { + return + } + if (!(context as any)?.style) { + ;(context as any).style = {} + } + ;(context as any).style.nonce = nonce + return Promise.resolve(nonce) + } + + if (!styleTag.callbacks) { + styleTag.callbacks = [] + } + styleTag.callbacks.push(storeNonce) + + return styleTag + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(Style as any)[DOM_RENDERER] = StyleRenderToDom diff --git a/src/jsx/dom/css.test.tsx b/src/jsx/dom/css.test.tsx index f7c3298f..5161af15 100644 --- a/src/jsx/dom/css.test.tsx +++ b/src/jsx/dom/css.test.tsx @@ -52,6 +52,18 @@ describe('Style and css for jsx/dom', () => { ) }) + it('') + }) + it('', async () => { const App = () => { return ( diff --git a/src/jsx/dom/css.ts b/src/jsx/dom/css.ts index c40eb6b1..5a3b3440 100644 --- a/src/jsx/dom/css.ts +++ b/src/jsx/dom/css.ts @@ -120,11 +120,12 @@ export const createCssJsxDomObjects: CreateCssJsxDomObjectsType = ({ id }) => { }, } - const Style: FC> = ({ children }) => + const Style: FC> = ({ children, nonce }) => ({ tag: 'style', props: { id, + nonce, children: children && (Array.isArray(children) ? children : [children]).map(