Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patch for cheerio performance #354

Merged
merged 1 commit into from Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/module.ts
Expand Up @@ -293,6 +293,15 @@ function registerSecurityNitroPlugins(nuxt: Nuxt, securityOptions: ModuleOptions
)
)

// Pre-process HTML into DOM tree
config.plugins.push(
normalize(
fileURLToPath(
new URL('./runtime/nitro/plugins/02a-preprocessHtml', import.meta.url)
)
)
)

// Register nitro plugin to enable Subresource Integrity
config.plugins.push(
normalize(
Expand Down Expand Up @@ -331,6 +340,16 @@ function registerSecurityNitroPlugins(nuxt: Nuxt, securityOptions: ModuleOptions
)
)
)


// Recombine HTML from DOM tree
config.plugins.push(
normalize(
fileURLToPath(
new URL('./runtime/nitro/plugins/99b-recombineHtml', import.meta.url)
)
)
)
})

// Make sure our nitro plugins will be applied last
Expand Down
24 changes: 24 additions & 0 deletions src/runtime/nitro/plugins/02a-preprocessHtml.ts
@@ -0,0 +1,24 @@
import { defineNitroPlugin, getRouteRules } from '#imports'
import * as cheerio from 'cheerio'


export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', async (html, { event }) => {

// Exit if no need to parse HTML for this route
const { security } = getRouteRules(event)
if (!security?.sri && (!security?.headers || !security?.headers.contentSecurityPolicy)) {
return
}

type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
const cheerios = {} as Record<Section, ReturnType<typeof cheerio.load>[]>
for (const section of sections) {
cheerios[section] = html[section].map(element => {
return cheerio.load(element, null, false)
})
}
event.context.cheerios = cheerios
})
})
10 changes: 4 additions & 6 deletions src/runtime/nitro/plugins/03-subresourceIntegrity.ts
@@ -1,10 +1,10 @@
import { useStorage, defineNitroPlugin, getRouteRules } from '#imports'
import * as cheerio from 'cheerio'
import { isPrerendering } from '../utils'
import { type CheerioAPI } from 'cheerio'


export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', async (html, { event }) => {

// Exit if SRI not enabled for this route
const { security } = getRouteRules(event)
if (!security?.sri) {
Expand All @@ -29,10 +29,9 @@ export default defineNitroPlugin((nitroApp) => {
// However the SRI standard provides that other elements may be added to that list in the future
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
const cheerios = event.context.cheerios as Record<Section, CheerioAPI[]>
for (const section of sections) {
html[section] = html[section].map(element => {

const $ = cheerio.load(element, null, false)
cheerios[section].forEach($ => {
// Add integrity to all relevant script tags
$('script').each((i, script) => {
const scriptAttrs = $(script).attr()
Expand Down Expand Up @@ -68,7 +67,6 @@ export default defineNitroPlugin((nitroApp) => {
}
}
})
return $.html()
})
}
})
Expand Down
10 changes: 4 additions & 6 deletions src/runtime/nitro/plugins/04-cspSsgHashes.ts
Expand Up @@ -22,18 +22,17 @@ export default defineNitroPlugin((nitroApp) => {
const scriptHashes: Set<string> = new Set()
const styleHashes: Set<string> = new Set()
const hashAlgorithm = 'sha256'
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const cheerios = event.context.cheerios as Record<Section, ReturnType<typeof cheerio.load>[]>

// Parse HTML if SSG is enabled for this route
if (security.ssg) {
const { hashScripts, hashStyles } = security.ssg

// Scan all relevant sections of the NuxtRenderHtmlContext
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
for (const section of sections) {
html[section].forEach(element => {
const $ = cheerio.load(element, null, false)

cheerios[section].forEach($ => {
// Parse all script tags
if (hashScripts) {
$('script').each((i, script) => {
Expand Down Expand Up @@ -103,10 +102,9 @@ export default defineNitroPlugin((nitroApp) => {
const csp = security.headers.contentSecurityPolicy
const headerValue = generateCspRules(csp, scriptHashes, styleHashes)
// Insert CSP in the http meta tag
html.head.push(`<meta http-equiv="Content-Security-Policy" content="${headerValue}">`)
cheerios.head.push(cheerio.load(`<meta http-equiv="Content-Security-Policy" content="${headerValue}">`))
// Update rules in HTTP header
setResponseHeader(event, 'Content-Security-Policy', headerValue)

})

// Insert hashes in the CSP meta tag for both the script-src and the style-src policies
Expand Down
7 changes: 3 additions & 4 deletions src/runtime/nitro/plugins/99-cspSsrNonce.ts
@@ -1,5 +1,5 @@
import { defineNitroPlugin, getRouteRules, setResponseHeader } from '#imports'
import * as cheerio from 'cheerio'
import { type CheerioAPI } from 'cheerio'
import type { ContentSecurityPolicyValue } from '~/src/module'
import { headerStringFromObject } from '../../utils/headers'
import { isPrerendering } from '../utils'
Expand All @@ -26,16 +26,15 @@ export default defineNitroPlugin((nitroApp) => {
// Scan all relevant sections of the NuxtRenderHtmlContext
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
const cheerios = event.context.cheerios as Record<Section, CheerioAPI[]>
for (const section of sections) {
html[section] = html[section].map(element => {
const $ = cheerio.load(element, null, false)
cheerios[section].forEach($ => {
// Add nonce to all link tags
$('link').attr('nonce', nonce)
// Add nonce to all script tags
$('script').attr('nonce', nonce)
// Add nonce to all style tags
$('style').attr('nonce', nonce)
return $.html()
})
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/runtime/nitro/plugins/99b-recombineHtml.ts
@@ -0,0 +1,25 @@
import { defineNitroPlugin, getRouteRules } from '#imports'
import { type CheerioAPI } from 'cheerio'


export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', (html, { event }) => {

// Exit if no need to parse HTML for this route
const { security } = getRouteRules(event)
if (!security?.sri && (!security?.headers || !security.headers.contentSecurityPolicy)) {
return
}

// Scan all relevant sections of the NuxtRenderHtmlContext
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
const cheerios = event.context.cheerios as Record<Section, CheerioAPI[]>
for (const section of sections) {
html[section] = cheerios[section].map($ => {
const html = $.html()
return html
})
}
})
})