@ -1,5 +1,5 @@
/** @import { Component } from 'svelte' */
/** @import { Component } from 'svelte' */
/** @import { HydratableContext, RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */
/** @import { Csp, HydratableContext, RenderOutput, SSRContext, SyncRenderOutput, Sha256Source } from './types.js' */
/** @import { MaybePromise } from '#shared' */
/** @import { MaybePromise } from '#shared' */
import { async _mode _flag } from '../flags/index.js' ;
import { async _mode _flag } from '../flags/index.js' ;
import { abort } from './abort-signal.js' ;
import { abort } from './abort-signal.js' ;
@ -9,7 +9,7 @@ import * as w from './warnings.js';
import { BLOCK _CLOSE , BLOCK _OPEN } from './hydration.js' ;
import { BLOCK _CLOSE , BLOCK _OPEN } from './hydration.js' ;
import { attributes } from './index.js' ;
import { attributes } from './index.js' ;
import { get _render _context , with _render _context , init _render _context } from './render-context.js' ;
import { get _render _context , with _render _context , init _render _context } from './render-context.js' ;
import { DEV } from 'esm-env ';
import { sha256 } from './crypto.js ';
/** @typedef {'head' | 'body'} RendererType */
/** @typedef {'head' | 'body'} RendererType */
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
@ -376,13 +376,13 @@ export class Renderer {
* 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 .
* 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 .
* @ template { Record < string , any > } Props
* @ template { Record < string , any > } Props
* @ param { Component < Props > } component
* @ param { Component < Props > } component
* @ param { { props ? : Omit < Props , '$$slots' | '$$events' > ; context ? : Map < any , any > ; idPrefix ? : string } } [ options ]
* @ param { { props ? : Omit < Props , '$$slots' | '$$events' > ; context ? : Map < any , any > ; idPrefix ? : string ; csp ? : Csp } } [ options ]
* @ returns { RenderOutput }
* @ returns { RenderOutput }
* /
* /
static render ( component , options = { } ) {
static render ( component , options = { } ) {
/** @type {AccumulatedContent | undefined} */
/** @type {AccumulatedContent | undefined} */
let sync ;
let sync ;
/** @type {Promise<AccumulatedContent > | undefined} */
/** @type {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } } > | undefined} */
let async ;
let async ;
const result = /** @type {RenderOutput} */ ( { } ) ;
const result = /** @type {RenderOutput} */ ( { } ) ;
@ -404,6 +404,11 @@ export class Renderer {
return ( sync ? ? = Renderer . # render ( component , options ) ) . body ;
return ( sync ? ? = Renderer . # render ( component , options ) ) . body ;
}
}
} ,
} ,
hashes : {
value : {
script : ''
}
} ,
then : {
then : {
value :
value :
/ * *
/ * *
@ -420,7 +425,8 @@ export class Renderer {
const user _result = onfulfilled ( {
const user _result = onfulfilled ( {
head : result . head ,
head : result . head ,
body : result . body ,
body : result . body ,
html : result . body
html : result . body ,
hashes : { script : [ ] }
} ) ;
} ) ;
return Promise . resolve ( user _result ) ;
return Promise . resolve ( user _result ) ;
}
}
@ -514,8 +520,8 @@ export class Renderer {
*
*
* @ template { Record < string , any > } Props
* @ template { Record < string , any > } Props
* @ param { Component < Props > } component
* @ param { Component < Props > } component
* @ param { { props ? : Omit < Props , '$$slots' | '$$events' > ; context ? : Map < any , any > ; idPrefix ? : string } } options
* @ param { { props ? : Omit < Props , '$$slots' | '$$events' > ; context ? : Map < any , any > ; idPrefix ? : string ; csp ? : Csp } } options
* @ returns { Promise < AccumulatedContent > }
* @ returns { Promise < AccumulatedContent & { hashes : { script : Sha256Source [ ] } } > }
* /
* /
static async # render _async ( component , options ) {
static async # render _async ( component , options ) {
const previous _context = ssr _context ;
const previous _context = ssr _context ;
@ -585,19 +591,19 @@ export class Renderer {
await comparison ;
await comparison ;
}
}
return await Renderer . # hydratable _block ( ctx ) ;
return await this . # hydratable _block ( ctx ) ;
}
}
/ * *
/ * *
* @ template { Record < string , any > } Props
* @ template { Record < string , any > } Props
* @ param { 'sync' | 'async' } mode
* @ param { 'sync' | 'async' } mode
* @ param { import ( 'svelte' ) . Component < Props > } component
* @ param { import ( 'svelte' ) . Component < Props > } component
* @ param { { props ? : Omit < Props , '$$slots' | '$$events' > ; context ? : Map < any , any > ; idPrefix ? : string } } options
* @ param { { props ? : Omit < Props , '$$slots' | '$$events' > ; context ? : Map < any , any > ; idPrefix ? : string ; csp ? : Csp } } options
* @ returns { Renderer }
* @ returns { Renderer }
* /
* /
static # open _render ( mode , component , options ) {
static # open _render ( mode , component , options ) {
const renderer = new Renderer (
const renderer = new Renderer (
new SSRState ( mode , options . idPrefix ? options . idPrefix + '-' : '' )
new SSRState ( mode , options . idPrefix ? options . idPrefix + '-' : '' , options . csp )
) ;
) ;
renderer . push ( BLOCK _OPEN ) ;
renderer . push ( BLOCK _OPEN ) ;
@ -623,6 +629,7 @@ export class Renderer {
/ * *
/ * *
* @ param { AccumulatedContent } content
* @ param { AccumulatedContent } content
* @ param { Renderer } renderer
* @ param { Renderer } renderer
* @ returns { AccumulatedContent & { hashes : { script : Sha256Source [ ] } } }
* /
* /
static # close _render ( content , renderer ) {
static # close _render ( content , renderer ) {
for ( const cleanup of renderer . # collect _on _destroy ( ) ) {
for ( const cleanup of renderer . # collect _on _destroy ( ) ) {
@ -638,14 +645,17 @@ export class Renderer {
return {
return {
head ,
head ,
body
body ,
hashes : {
script : renderer . global . csp . script _hashes
}
} ;
} ;
}
}
/ * *
/ * *
* @ param { HydratableContext } ctx
* @ param { HydratableContext } ctx
* /
* /
static async # hydratable _block ( ctx ) {
async # hydratable _block ( ctx ) {
if ( ctx . lookup . size === 0 ) {
if ( ctx . lookup . size === 0 ) {
return null ;
return null ;
}
}
@ -669,9 +679,7 @@ export class Renderer {
$ { prelude } ` ;
$ { prelude } ` ;
}
}
// TODO csp -- have discussed but not implemented
const body = `
return `
< script >
{
{
$ { prelude }
$ { prelude }
@ -681,11 +689,27 @@ export class Renderer {
h . set ( k , v ) ;
h . set ( k , v ) ;
}
}
}
}
< / s c r i p t > ` ;
` ;
let csp _attr = '' ;
if ( this . global . csp . nonce ) {
csp _attr = ` nonce=" ${ this . global . csp . nonce } " ` ;
} else if ( this . global . csp . hash ) {
// note to future selves: this doesn't need to be optimized with a Map<body, hash>
// because the it's impossible for identical data to occur multiple times in a single render
// (this would require the same hydratable key:value pair to be serialized multiple times)
const hash = await sha256 ( body ) ;
this . global . csp . script _hashes . push ( ` sha256- ${ hash } ` ) ;
}
return ` \n \t \t <script ${ csp _attr } > ${ body } </script> ` ;
}
}
}
}
export class SSRState {
export class SSRState {
/** @readonly @type {Csp & { script_hashes: Sha256Source[] }} */
csp ;
/** @readonly @type {'sync' | 'async'} */
/** @readonly @type {'sync' | 'async'} */
mode ;
mode ;
@ -700,10 +724,12 @@ export class SSRState {
/ * *
/ * *
* @ param { 'sync' | 'async' } mode
* @ param { 'sync' | 'async' } mode
* @ param { string } [ id _prefix ]
* @ param { string } id _prefix
* @ param { Csp } csp
* /
* /
constructor ( mode , id _prefix = '' ) {
constructor ( mode , id _prefix = '' , csp = { hash : false } ) {
this . mode = mode ;
this . mode = mode ;
this . csp = { ... csp , script _hashes : [ ] } ;
let uid = 1 ;
let uid = 1 ;
this . uid = ( ) => ` ${ id _prefix } s ${ uid ++ } ` ;
this . uid = ( ) => ` ${ id _prefix } s ${ uid ++ } ` ;