From 5f7e640929e3f36fd8cecc03db49a90b176c9c39 Mon Sep 17 00:00:00 2001
From: Moritz Eck
Date: Tue, 19 Nov 2024 00:53:43 +0100
Subject: [PATCH 1/7] 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 4b58a34a9..414869df0 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 = (
+
+
+
+
+
+
+
+
+ )
+
+ const awaitedHtml = await html
+ const htmlEscapedString = 'callbacks' in awaitedHtml ? awaitedHtml : await awaitedHtml.toString()
+ assertEquals(
+ await resolveCallback(htmlEscapedString, HtmlEscapedCallbackPhase.Stringify, false, {}),
+ ''
+ )
+})
+
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 = (
+ <>
+
+
+ >
+ )
+ expect(await toString(template)).toBe(
+ 'Hello!
'
+ )
+ })
})
})
}
diff --git a/src/helper/css/index.test.tsx b/src/helper/css/index.test.tsx
index 679f99e15..3e84a9529 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' })}
+ `
+ expect(await toString(template)).toBe(
+ `
+ Hello!
`
+ )
+ })
})
describe('cx()', () => {
diff --git a/src/helper/css/index.ts b/src/helper/css/index.ts
index f1aa02a8d..417923afc 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 = ``
+
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 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('', async () => {
+ const App = () => {
+ return (
+
+
+
+ )
+ }
+ render(, root)
+ expect(root.innerHTML).toBe('')
+ })
+
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(
From f82e191dda3c4c4a6e647e7c0c576b6ee6d6ba22 Mon Sep 17 00:00:00 2001
From: Taku Amano
Date: Sat, 23 Nov 2024 05:54:23 +0900
Subject: [PATCH 2/7] test(helper/css): fix expected result
---
runtime-tests/deno-jsx/jsx.test.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/runtime-tests/deno-jsx/jsx.test.tsx b/runtime-tests/deno-jsx/jsx.test.tsx
index 414869df0..c0399afce 100644
--- a/runtime-tests/deno-jsx/jsx.test.tsx
+++ b/runtime-tests/deno-jsx/jsx.test.tsx
@@ -130,7 +130,7 @@ Deno.test('JSX: css with CSP nonce', async () => {
const htmlEscapedString = 'callbacks' in awaitedHtml ? awaitedHtml : await awaitedHtml.toString()
assertEquals(
await resolveCallback(htmlEscapedString, HtmlEscapedCallbackPhase.Stringify, false, {}),
- ''
+ ''
)
})
From 671384726b72cea9a747ed00e6e900d2c307692c Mon Sep 17 00:00:00 2001
From: Taku Amano
Date: Sat, 23 Nov 2024 05:55:04 +0900
Subject: [PATCH 3/7] test(helper/css): add test for stream with nonce
---
src/helper/css/index.test.tsx | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/src/helper/css/index.test.tsx b/src/helper/css/index.test.tsx
index 3e84a9529..c8001d9c3 100644
--- a/src/helper/css/index.test.tsx
+++ b/src/helper/css/index.test.tsx
@@ -239,6 +239,23 @@ describe('CSS Helper', () => {
})
})
+ app.get('/stream-with-nonce', (c) => {
+ const stream = renderToReadableStream(
+ <>
+
+ Loading...
}>
+
+
+ >
+ )
+ return c.body(stream, {
+ headers: {
+ 'Content-Type': 'text/html; charset=UTF-8',
+ 'Transfer-Encoding': 'chunked',
+ },
+ })
+ })
+
it('/sync', async () => {
const res = await app.request('http://localhost/sync')
expect(res).not.toBeNull()
@@ -259,6 +276,22 @@ if(!d)return
do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
d.replaceWith(c.content)
})(document)
+`
+ )
+ })
+
+ it('/stream-with-nonce', async () => {
+ const res = await app.request('http://localhost/stream-with-nonce')
+ expect(res).not.toBeNull()
+ expect(await res.text()).toBe(
+ `Loading...
Hello!
`
)
})
From 771d785516a5a0df3839c9640c02449b3242e8a3 Mon Sep 17 00:00:00 2001
From: Taku Amano
Date: Sat, 23 Nov 2024 05:55:23 +0900
Subject: [PATCH 4/7] fix(helper/css): Update regex to match nonce attribute
---
src/helper/css/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/helper/css/index.ts b/src/helper/css/index.ts
index 417923afc..4f0e7d669 100644
--- a/src/helper/css/index.ts
+++ b/src/helper/css/index.ts
@@ -63,7 +63,7 @@ export const createCssContext = ({ id }: { id: Readonly }): DefaultConte
const contextMap: WeakMap