0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-21 18:18:57 +01:00

feat(css): add CSP nonce to hono/css related style and script tags

This commit is contained in:
Moritz Eck 2024-11-19 00:53:43 +01:00
parent c8f6a866c0
commit 5f7e640929
6 changed files with 102 additions and 10 deletions

View File

@ -111,6 +111,29 @@ Deno.test('JSX: css', async () => {
)
})
Deno.test('JSX: css with CSP nonce', async () => {
const className = css`
color: red;
`
const html = (
<html>
<head>
<Style nonce='1234' />
</head>
<body>
<div class={className}></div>
</body>
</html>
)
const awaitedHtml = await html
const htmlEscapedString = 'callbacks' in awaitedHtml ? awaitedHtml : await awaitedHtml.toString()
assertEquals(
await resolveCallback(htmlEscapedString, HtmlEscapedCallbackPhase.Stringify, false, {}),
'<html><head><style nonce="1234" id="hono-css">.css-3142110215{color:red}</style></head><body><div class="css-3142110215"></div></body></html>'
)
})
Deno.test('JSX: normalize key', async () => {
const className = <div className='foo'></div>
const htmlFor = <div htmlFor='foo'></div>

View File

@ -488,6 +488,21 @@ export const renderTest = (
'<style id="hono-css">.css-478287868{padding:0}</style><h1 class="css-478287868">Hello!</h1>'
)
})
it('Should render CSS styles with CSP nonce', async () => {
const headerClass = css`
background-color: blue;
`
const template = (
<>
<Style nonce='1234' />
<h1 class={headerClass}>Hello!</h1>
</>
)
expect(await toString(template)).toBe(
'<style id="hono-css" nonce="1234">.css-2458908649{background-color:blue}</style><h1 class="css-2458908649">Hello!</h1>'
)
})
})
})
}

View File

@ -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', () => {
<h1 class="css-2458908649">Hello!</h1>`
)
})
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' })}
<h1 class="${headerClass}">Hello!</h1>`
expect(await toString(template)).toBe(
`<style id="hono-css" nonce="1234">.css-2458908649{background-color:blue}</style>
<h1 class="css-2458908649">Hello!</h1>`
)
})
})
describe('cx()', () => {

View File

@ -50,7 +50,7 @@ interface ViewTransitionType {
}
interface StyleType {
(args?: { children?: Promise<string> }): HtmlEscapedString
(args?: { children?: Promise<string>; nonce?: string }): HtmlEscapedString
}
/**
@ -88,9 +88,12 @@ export const createCssContext = ({ id }: { id: Readonly<string> }): DefaultConte
return
}
const appendStyleScript = `<script>document.querySelector('#${id}').textContent+=${JSON.stringify(
stylesStr
)}</script>`
const styleNonce = (context as any)?.style?.nonce
const appendStyleScript = `<script${
styleNonce ? ` nonce="${styleNonce}"` : ''
}>document.querySelector('#${id}').textContent+=${JSON.stringify(stylesStr)}</script>`
if (buffer) {
buffer[0] = `${appendStyleScript}${buffer[0]}`
return
@ -156,10 +159,36 @@ export const createCssContext = ({ id }: { id: Readonly<string> }): DefaultConte
return newCssClassNameObject(viewTransitionCommon(strings as any, values))
}) as ViewTransitionType
const Style: StyleType = ({ children } = {}) =>
children
? raw(`<style id="${id}">${(children as unknown as CssClassName)[STYLE_STRING]}</style>`)
: raw(`<style id="${id}"></style>`)
const Style: StyleType = ({ children, nonce } = {}) => {
const styleTag = children
? raw(
`<style id="${id}"${nonce ? ` nonce="${nonce}"` : ''}>${
(children as unknown as CssClassName)[STYLE_STRING]
}</style>`
)
: raw(`<style id="${id}"${nonce ? ` nonce="${nonce}"` : ''}></style>`)
;(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

View File

@ -52,6 +52,18 @@ describe('Style and css for jsx/dom', () => {
)
})
it('<Style nonce="1234" />', async () => {
const App = () => {
return (
<div>
<Style nonce='1234' />
</div>
)
}
render(<App />, root)
expect(root.innerHTML).toBe('<div><style id="hono-css" nonce="1234"></style></div>')
})
it('<Style>{css`global`}</Style>', async () => {
const App = () => {
return (

View File

@ -120,11 +120,12 @@ export const createCssJsxDomObjects: CreateCssJsxDomObjectsType = ({ id }) => {
},
}
const Style: FC<PropsWithChildren<void>> = ({ children }) =>
const Style: FC<PropsWithChildren<{ nonce?: string }>> = ({ children, nonce }) =>
({
tag: 'style',
props: {
id,
nonce,
children:
children &&
(Array.isArray(children) ? children : [children]).map(