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

feat: Extract initialState to asset to respect CSP #67

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
39 changes: 7 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Critical CSS generation will be enabled automatically for you.

The initial state comprises data that is serialized to your server-side generated HTML that is hydrated in
the browser when accessed. This data can be data fetched from a CDN, an API, etc, and is typically needed
as soon as the application starts or is accessed for the first time.
as soon as the application **starts** or is accessed for the **first time**.

The main advantage of setting the application's initial state is that the statically generated pages do not
need to fetch the data again as the data is fetched during build time and serialized into the page's HTML.
Expand Down Expand Up @@ -168,13 +168,9 @@ export const createApp = ViteSSG(
pinia.state.value = initialState.pinia || {}
}

router.beforeEach((to, from, next) => {
const store = useRootStore(pinia)
if (!store.ready)
// perform the (user-implemented) store action to fill the store's state
store.initialize()
next()
})
if (!store.ready)
// perform the (user-implemented) store action to fill the store's state
store.initialize()
},
)
```
Expand Down Expand Up @@ -208,13 +204,9 @@ export const createApp = ViteSSG(
store.replaceState(initialState.store)
}

router.beforeEach((to, from, next) => {
// perform the (user-implemented) store action to fill the store's state
if (!store.getters.ready)
store.dispatch('initialize')

next()
})
// perform the (user-implemented) store action to fill the store's state
if (!store.getters.ready)
store.dispatch('initialize')
},
)
```
Expand Down Expand Up @@ -253,23 +245,6 @@ export const createApp = ViteSSG(
)
```

**A minor remark when using `@nuxt/devalue`:** In case, you are getting an error because of a `require`
within the package `@nuxt/devalue`, you have to add the following piece of config to your Vite config:

```ts
// vite.config.ts
//...

export default defineConfig({
resolve: {
alias: {
'@nuxt/devalue': '@nuxt/devalue/dist/devalue.js',
},
},
// ...
})
```

## Configuration

You can pass options to Vite SSG in the `ssgOptions` field of your `vite.config.js`
Expand Down
8 changes: 2 additions & 6 deletions examples/multiple-pages-with-store/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,8 @@ export const createApp = ViteSSG(
pinia.state.value = initialState?.pinia || {}
}

router.beforeEach((to, from, next) => {
const store = useRootStore(pinia)

store.initialize()
next()
})
const store = useRootStore(pinia)
store.initialize()
},
{
transformState(state) {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"devDependencies": {
"@antfu/eslint-config": "^0.7.0",
"@types/fs-extra": "^9.0.12",
"@types/hash-sum": "^1.0.0",
"@types/jsdom": "^16.2.13",
"@types/yargs": "^17.0.2",
"@typescript-eslint/eslint-plugin": "^4.30.0",
Expand All @@ -95,6 +96,7 @@
"critters": "^0.0.10",
"eslint": "^7.32.0",
"esno": "^0.9.1",
"hash-sum": "^2.0.0",
"rollup": "^2.56.3",
"standard-version": "^9.3.1",
"tsup": "^4.14.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 26 additions & 7 deletions src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join, dirname, isAbsolute, parse } from 'path'
import chalk from 'chalk'
import fs from 'fs-extra'
import { build as viteBuild, resolveConfig, ResolvedConfig } from 'vite'
import hash_sum from 'hash-sum'
import { renderToString, SSRContext } from '@vue/server-renderer'
import { JSDOM, VirtualConsole } from 'jsdom'
import { RollupOutput } from 'rollup'
Expand Down Expand Up @@ -130,14 +131,22 @@ export async function build(cliOptions: Partial<ViteSSGOptions> = {}) {
const appHTML = await renderToString(app, ctx)

// need to resolve assets so render content first
const renderedHTML = renderHTML({ indexHTML: transformedIndexHTML, appHTML, initialState })
const renderedHTML = renderHTML({ indexHTML: transformedIndexHTML, appHTML })

// create jsdom from renderedHTML
const jsdom = new JSDOM(renderedHTML)

// render current page's preloadLinks
renderPreloadLinks(jsdom.window.document, ctx.modules || new Set<string>(), ssrManifest)

const relativeRoute = (route.endsWith('/') ? `${route}index` : route).replace(/^\//g, '')

// add initial state as an asset
if (initialState && Object.keys(initialState).length !== 0) {
const initialStatePath = await createInitialState({ initialState, out })
await addInitialState({ jsdom, initialStatePath })
}

// render head
head?.updateDOM(jsdom.window.document)

Expand All @@ -148,7 +157,6 @@ export async function build(cliOptions: Partial<ViteSSGOptions> = {}) {

const formatted = format(transformed, formatting)

const relativeRoute = (route.endsWith('/') ? `${route}index` : route).replace(/^\//g, '')
const filename = dirStyle === 'nested'
? join(relativeRoute, 'index.html')
: `${relativeRoute}.html`
Expand Down Expand Up @@ -182,17 +190,28 @@ function rewriteScripts(indexHTML: string, mode?: string) {
return indexHTML.replace(/<script type="module" /g, `<script type="module" ${mode} `)
}

function renderHTML({ indexHTML, appHTML, initialState }: { indexHTML: string; appHTML: string; initialState: any }) {
const stateScript = initialState
? `\n<script>window.__INITIAL_STATE__=${initialState}</script>`
: ''
function renderHTML({ indexHTML, appHTML }: { indexHTML: string; appHTML: string }) {
return indexHTML
.replace(
'<div id="app"></div>',
`<div id="app" data-server-rendered="true">${appHTML}</div>${stateScript}`,
`<div id="app" data-server-rendered="true">${appHTML}</div>`,
)
}

async function createInitialState({ initialState, out }: { initialState: any; out: string }) {
const initialStateScript = `window.__INITIAL_STATE__ = ${initialState}`
const initialStatePath = join('assets', `initial-state.${hash_sum(initialStateScript)}.js`)
await fs.writeFile(join(out, initialStatePath), initialStateScript, 'utf-8')
return initialStatePath
}

async function addInitialState({ jsdom, initialStatePath }: { jsdom: JSDOM; initialStatePath: string }) {
const initialStateScriptTag = jsdom.window.document.createElement('script')
initialStateScriptTag.defer = true
initialStateScriptTag.src = initialStatePath
jsdom.window.document.head.prepend(initialStateScriptTag)
}

function format(html: string, formatting: ViteSSGOptions['formatting']) {
if (formatting === 'minify') {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand Down