diff --git a/runtime-tests/deno-jsx/jsx.test.tsx b/runtime-tests/deno-jsx/jsx.test.tsx index 4b58a34a9..c0399afce 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 74422944c..112acd190 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 679f99e15..c8001d9c3 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()', () => { @@ -227,6 +239,23 @@ describe('CSS Helper', () => { }) }) + app.get('/stream-with-nonce', (c) => { + const stream = renderToReadableStream( + <> +

Loading...

` ) }) diff --git a/src/helper/css/index.ts b/src/helper/css/index.ts index f1aa02a8d..76437d597 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 } /** @@ -62,8 +62,9 @@ export const createCssContext = ({ id }: { id: Readonly }): DefaultConte const [cssJsxDomObject, StyleRenderToDom] = createCssJsxDomObjects({ id }) const contextMap: WeakMap = new WeakMap() + const nonceMap: WeakMap = new WeakMap() - const replaceStyleRe = new RegExp(`()`) + const replaceStyleRe = new RegExp(`()`) const newCssClassNameObject = (cssClassName: CssClassNameCommon): Promise => { const appendStyle: HtmlEscapedCallback = ({ buffer, context }): Promise | undefined => { @@ -88,9 +89,11 @@ export const createCssContext = ({ id }: { id: Readonly }): DefaultConte return } - const appendStyleScript = `` + const nonce = nonceMap.get(context) + const appendStyleScript = `document.querySelector('#${id}').textContent+=${JSON.stringify(stylesStr)}` + if (buffer) { buffer[0] = `${appendStyleScript}${buffer[0]}` return @@ -100,7 +103,7 @@ export const createCssContext = ({ id }: { id: Readonly }): DefaultConte } const addClassNameToContext: HtmlEscapedCallback = ({ context }) => { - if (!contextMap.get(context)) { + if (!contextMap.has(context)) { contextMap.set(context, [{}, {}]) } const [toAdd, added] = contextMap.get(context) as usedClassNameData @@ -156,10 +159,19 @@ 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 } = {}) => + raw( + ``, + [ + ({ context }) => { + nonceMap.set(context, nonce) + return undefined + }, + ] + ) + // 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 f7c3298fd..5161af156 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 c40eb6b1c..5a3b34403 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( diff --git a/src/utils/html.ts b/src/utils/html.ts index 7731e565c..855fa160f 100644 --- a/src/utils/html.ts +++ b/src/utils/html.ts @@ -11,7 +11,7 @@ export const HtmlEscapedCallbackPhase = { type HtmlEscapedCallbackOpts = { buffer?: [string] phase: (typeof HtmlEscapedCallbackPhase)[keyof typeof HtmlEscapedCallbackPhase] - context: object // An object unique to each JSX tree. This object is used as the WeakMap key. + context: Readonly // An object unique to each JSX tree. This object is used as the WeakMap key. } export type HtmlEscapedCallback = (opts: HtmlEscapedCallbackOpts) => Promise | undefined export type HtmlEscaped = {