Skip to content

Commit

Permalink
feat(css): add CSP nonce to hono/css related style and script tags
Browse files Browse the repository at this point in the history
  • Loading branch information
meck93 committed Nov 18, 2024
1 parent c8f6a86 commit 5f7e640
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 10 deletions.
23 changes: 23 additions & 0 deletions runtime-tests/deno-jsx/jsx.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
15 changes: 15 additions & 0 deletions src/helper/css/common.case.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Check failure on line 502 in src/helper/css/common.case.test.tsx

View workflow job for this annotation

GitHub Actions / Main

src/helper/css/index.test.tsx > CSS Helper > render css > Booleans, Null, and Undefined Are Ignored > Should render CSS styles with CSP nonce

AssertionError: expected '<script nonce="1234">document.querySe…' to be '<style id="hono-css" nonce="1234">.cs…' // Object.is equality Expected: "<style id="hono-css" nonce="1234">.css-2458908649{background-color:blue}</style><h1 class="css-2458908649">Hello!</h1>" Received: "<script nonce="1234">document.querySelector('#hono-css').textContent+=".css-2458908649{background-color:blue}"</script><style id="hono-css" nonce="1234"></style><h1 class="css-2458908649">Hello!</h1>" ❯ src/helper/css/common.case.test.tsx:502:42
'<style id="hono-css" nonce="1234">.css-2458908649{background-color:blue}</style><h1 class="css-2458908649">Hello!</h1>'
)
})
})
})
}
14 changes: 13 additions & 1 deletion src/helper/css/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(

Check failure on line 68 in src/helper/css/index.test.tsx

View workflow job for this annotation

GitHub Actions / Main

src/helper/css/index.test.tsx > CSS Helper > with `html` tag function > Should render CSS styles with `html` tag function and CSP nonce

AssertionError: expected '<script nonce="1234">document.querySe…' to be '<style id="hono-css" nonce="1234">.cs…' // Object.is equality - Expected + Received - <style id="hono-css" nonce="1234">.css-2458908649{background-color:blue}</style> + <script nonce="1234">document.querySelector('#hono-css').textContent+=".css-2458908649{background-color:blue}"</script><style id="hono-css" nonce="1234"></style> <h1 class="css-2458908649">Hello!</h1> ❯ src/helper/css/index.test.tsx:68:40
`<style id="hono-css" nonce="1234">.css-2458908649{background-color:blue}</style>
<h1 class="css-2458908649">Hello!</h1>`
)
})
})

describe('cx()', () => {
Expand Down
45 changes: 37 additions & 8 deletions src/helper/css/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ interface ViewTransitionType {
}

interface StyleType {
(args?: { children?: Promise<string> }): HtmlEscapedString
(args?: { children?: Promise<string>; nonce?: string }): HtmlEscapedString
}

/**
Expand Down Expand Up @@ -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

Check warning on line 91 in src/helper/css/index.ts

View workflow job for this annotation

GitHub Actions / Main

Unexpected any. Specify a different type

const appendStyleScript = `<script${
styleNonce ? ` nonce="${styleNonce}"` : ''
}>document.querySelector('#${id}').textContent+=${JSON.stringify(stylesStr)}</script>`

if (buffer) {
buffer[0] = `${appendStyleScript}${buffer[0]}`
return
Expand Down Expand Up @@ -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

Check warning on line 171 in src/helper/css/index.ts

View workflow job for this annotation

GitHub Actions / Main

Unexpected any. Specify a different type

const storeNonce: HtmlEscapedCallback = ({ context }) => {
if (!nonce) {
return
}
if (!(context as any)?.style) {

Check warning on line 177 in src/helper/css/index.ts

View workflow job for this annotation

GitHub Actions / Main

Unexpected any. Specify a different type
;(context as any).style = {}

Check warning on line 178 in src/helper/css/index.ts

View workflow job for this annotation

GitHub Actions / Main

Unexpected any. Specify a different type
}
;(context as any).style.nonce = nonce

Check warning on line 180 in src/helper/css/index.ts

View workflow job for this annotation

GitHub Actions / Main

Unexpected any. Specify a different type
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

Expand Down
12 changes: 12 additions & 0 deletions src/jsx/dom/css.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
3 changes: 2 additions & 1 deletion src/jsx/dom/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 5f7e640

Please sign in to comment.