11/** @import { Component } from 'svelte' */
2- /** @import { HydratableContext, RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */
2+ /** @import { CspInternal, HydratableContext, RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */
33/** @import { MaybePromise } from '#shared' */
44import { async_mode_flag } from '../flags/index.js' ;
55import { abort } from './abort-signal.js' ;
@@ -9,7 +9,7 @@ import * as w from './warnings.js';
99import { BLOCK_CLOSE , BLOCK_OPEN } from './hydration.js' ;
1010import { attributes } from './index.js' ;
1111import { get_render_context , with_render_context , init_render_context } from './render-context.js' ;
12- import { DEV } from 'esm-env ' ;
12+ import { sha256 } from './crypto.js ' ;
1313
1414/** @typedef {'head' | 'body' } RendererType */
1515/** @typedef {{ [key in RendererType]: string } } AccumulatedContent */
@@ -376,7 +376,7 @@ export class Renderer {
376376 * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
377377 * @template {Record<string, any>} Props
378378 * @param {Component<Props> } component
379- * @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string } } [options]
379+ * @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string; csp?: CspInternal } } [options]
380380 * @returns {RenderOutput }
381381 */
382382 static render ( component , options = { } ) {
@@ -404,6 +404,11 @@ export class Renderer {
404404 return ( sync ??= Renderer . #render( component , options ) ) . body ;
405405 }
406406 } ,
407+ hashes : {
408+ value : {
409+ script : ''
410+ }
411+ } ,
407412 then : {
408413 value :
409414 /**
@@ -420,7 +425,8 @@ export class Renderer {
420425 const user_result = onfulfilled ( {
421426 head : result . head ,
422427 body : result . body ,
423- html : result . body
428+ html : result . body ,
429+ hashes : { script : '' }
424430 } ) ;
425431 return Promise . resolve ( user_result ) ;
426432 }
@@ -514,8 +520,8 @@ export class Renderer {
514520 *
515521 * @template {Record<string, any>} Props
516522 * @param {Component<Props> } component
517- * @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string } } options
518- * @returns {Promise<AccumulatedContent> }
523+ * @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string; csp?: CspInternal } } options
524+ * @returns {Promise<AccumulatedContent & { hashes: { script: string } } > }
519525 */
520526 static async #render_async( component , options ) {
521527 const previous_context = ssr_context ;
@@ -585,19 +591,19 @@ export class Renderer {
585591 await comparison ;
586592 }
587593
588- return await Renderer . #hydratable_block( ctx ) ;
594+ return await this . #hydratable_block( ctx ) ;
589595 }
590596
591597 /**
592598 * @template {Record<string, any>} Props
593599 * @param {'sync' | 'async' } mode
594600 * @param {import('svelte').Component<Props> } component
595- * @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string } } options
601+ * @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string; csp?: CspInternal } } options
596602 * @returns {Renderer }
597603 */
598604 static #open_render( mode , component , options ) {
599605 const renderer = new Renderer (
600- new SSRState ( mode , options . idPrefix ? options . idPrefix + '-' : '' )
606+ new SSRState ( mode , options . idPrefix ? options . idPrefix + '-' : '' , options . csp )
601607 ) ;
602608
603609 renderer . push ( BLOCK_OPEN ) ;
@@ -623,6 +629,7 @@ export class Renderer {
623629 /**
624630 * @param {AccumulatedContent } content
625631 * @param {Renderer } renderer
632+ * @returns {AccumulatedContent & { hashes: { script: string } } }
626633 */
627634 static #close_render( content , renderer ) {
628635 for ( const cleanup of renderer . #collect_on_destroy( ) ) {
@@ -638,14 +645,17 @@ export class Renderer {
638645
639646 return {
640647 head,
641- body
648+ body,
649+ hashes : {
650+ script : renderer . global . csp . script_hashes . map ( ( hash ) => `'${ hash } '` ) . join ( ' ' )
651+ }
642652 } ;
643653 }
644654
645655 /**
646656 * @param {HydratableContext } ctx
647657 */
648- static async #hydratable_block( ctx ) {
658+ async #hydratable_block( ctx ) {
649659 if ( ctx . lookup . size === 0 ) {
650660 return null ;
651661 }
@@ -665,27 +675,37 @@ export class Renderer {
665675 let prelude = `const h = (window.__svelte ??= {}).h ??= new Map();` ;
666676
667677 if ( has_promises ) {
668- prelude = `const r = (v) => Promise.resolve(v);
669- ${ prelude } ` ;
678+ prelude = `const r = (v) => Promise.resolve(v);\n\t${ prelude } ` ;
670679 }
671680
672- // TODO csp -- have discussed but not implemented
673- return `
674- <script>
675- {
676- ${ prelude }
681+ const body = `
682+ {
683+ ${ prelude }
677684
678- for (const [k, v] of [
679- ${ entries . join ( ',\n\t\t\t\t\t' ) }
680- ]) {
681- h.set(k, v);
682- }
683- }
684- </script>` ;
685+ for (const [k, v] of [
686+ ${ entries . join ( ',\n' ) }
687+ ]) {
688+ h.set(k, v);
689+ }
690+ }
691+ ` ;
692+
693+ let csp_attr = '' ;
694+ if ( this . global . csp . nonce ) {
695+ csp_attr = ` nonce="${ this . global . csp . nonce } "` ;
696+ } else if ( this . global . csp . hash ) {
697+ const hash = await sha256 ( body ) ;
698+ this . global . csp . script_hashes . push ( hash ) ;
699+ }
700+
701+ return `<script${ csp_attr } >${ body } </script>` ;
685702 }
686703}
687704
688705export class SSRState {
706+ /** @readonly @type {CspInternal & { script_hashes: string[] } } */
707+ csp ;
708+
689709 /** @readonly @type {'sync' | 'async' } */
690710 mode ;
691711
@@ -701,9 +721,11 @@ export class SSRState {
701721 /**
702722 * @param {'sync' | 'async' } mode
703723 * @param {string } [id_prefix]
724+ * @param {CspInternal } [csp]
704725 */
705- constructor ( mode , id_prefix = '' ) {
726+ constructor ( mode , id_prefix = '' , csp = { hash : false } ) {
706727 this . mode = mode ;
728+ this . csp = { ...csp , script_hashes : [ ] } ;
707729
708730 let uid = 1 ;
709731 this . uid = ( ) => `${ id_prefix } s${ uid ++ } ` ;
0 commit comments