refactor: SSR

This commit is contained in:
Guillaume Chau
2018-08-30 13:36:15 +02:00
parent 62829e4a96
commit 768e4688b9
16 changed files with 473 additions and 335 deletions
+1
View File
@@ -76,6 +76,7 @@ module.exports = {
children: [
'apollo-provider',
'dollar-apollo',
'ssr',
]
},
{
-98
View File
@@ -45,101 +45,3 @@ new Vue({
render: h => h(App),
})
```
## Methods
### provide
Use this to inject the provider into an app:
```js
new Vue({
el: '#app',
provide: apolloProvider.provide(),
render: h => h(App),
})
```
### prefetchAll
(SSR) Prefetch all queued component definitions and returns a promise resolved when all corresponding apollo data is ready.
```js
await apolloProvider.prefetchAll (context, componentDefs, options)
```
`context` is passed as the argument to the `prefetch` options inside the smart queries. It may contain the route and the store.
`options` defaults to:
```js
{
// Include components outside of the routes
// that are registered with `willPrefetch`
includeGlobal: true,
}
```
### getStates
(SSR) Returns the apollo stores states as JavaScript objects.
```js
const states = apolloProvider.getStates(options)
```
`options` defaults to:
```js
{
// Prefix for the keys of each apollo client state
exportNamespace: '',
}
```
### exportStates
(SSR) Returns the apollo stores states as JavaScript code inside a String. This code can be directly injected to the page HTML inside a `<script>` tag.
```js
const js = apolloProvider.exportStates(options)
```
`options` defaults to:
```js
{
// Global variable name
globalName: '__APOLLO_STATE__',
// Global object on which the variable is set
attachTo: 'window',
// Prefix for the keys of each apollo client state
exportNamespace: '',
}
```
## Other methods
### willPrefetch
Tells vue-apollo that some components not used in a `router-view` (and thus, not in vue-router `matchedComponents`) need to be prefetched, with the `willPrefetch` method:
```js
import { willPrefetch } from 'vue-apollo'
export default willPrefetch({
apollo: {
allPosts: {
query: gql`query AllPosts {
allPosts {
id
imageUrl
description
}
}`,
prefetch: true, // Don't forget this
}
}
})
```
+55
View File
@@ -0,0 +1,55 @@
# ApolloSSR
## Usage
See [SSR guide](../guide/ssr.md).
## Methods
### prefetchAll
Prefetches all queued component definitions and returns a promise resolved when all corresponding apollo data is ready.
```js
await ApolloSSR.prefetchAll (apolloProvider, componentDefs, context)
```
`context` is passed as the argument to the `prefetch` options inside the smart queries. It may contain the route and the store.
### getStates
Returns the apollo stores states as JavaScript objects.
```js
const states = ApolloSSR.getStates(apolloProvider, options)
```
`options` defaults to:
```js
{
// Prefix for the keys of each apollo client state
exportNamespace: '',
}
```
### exportStates
Returns the apollo stores states as JavaScript code inside a String. This code can be directly injected to the page HTML inside a `<script>` tag.
```js
const js = ApolloSSR.exportStates(apolloProvider, options)
```
`options` defaults to:
```js
{
// Global variable name
globalName: '__APOLLO_STATE__',
// Global object on which the variable is set
attachTo: 'window',
// Prefix for the keys of each apollo client state
exportNamespace: '',
}
```
+64 -43
View File
@@ -5,12 +5,12 @@
On the queries you want to prefetch on the server, add the `prefetch` option. It can either be:
- a variables object,
- a function that gets the context object (which can contain the URL for example) and return a variables object,
- `true` (query's `variables` is reused).
- `false` to disable prefetching for this query.
If you are returning a variables object in the `prefetch` option, make sure it matches the result of the `variables` option. If they do not match, the query's data property will not be populated while rendering the template server-side.
::: danger
You don't have access to the component instance when doing prefetching on the server. Don't use `this` in `prefetch`!
You don't have access to the component instance when doing prefetching on the server.
:::
Example:
@@ -19,6 +19,7 @@ Example:
export default {
apollo: {
allPosts: {
// This will be prefetched
query: gql`query AllPosts {
allPosts {
id
@@ -26,7 +27,6 @@ export default {
description
}
}`,
prefetch: true,
}
}
}
@@ -60,12 +60,12 @@ export default {
}
```
You can also tell vue-apollo that some components not used in a `router-view` (and thus, not in vue-router `matchedComponents`) need to be prefetched, with the `willPrefetch` method:
### Skip prefetching
Example that doesn't prefetch the query:
```js
import { willPrefetch } from 'vue-apollo'
export default willPrefetch({
export default {
apollo: {
allPosts: {
query: gql`query AllPosts {
@@ -75,27 +75,60 @@ export default willPrefetch({
description
}
}`,
prefetch: true, // Don't forget this
// Don't prefetch
prefetch: false,
}
}
})
}
```
The second parameter is optional: it's a callback that gets the context and should return a boolean indicating if the component should be prefetched:
If you want to skip prefetching all the queries for a specific component, use the `$prefetch` option:
```js
willPrefetch({
// Component definition...
}, context => context.url === '/foo')
export default {
apollo: {
// Don't prefetch any query
$prefetch: false,
allPosts: {
query: gql`query AllPosts {
allPosts {
id
imageUrl
description
}
}`,
}
}
}
```
You can also put a `no-prefetch` attribute on any component so it will be ignored while walking the tree to gather the Apollo queries:
```vue
<ApolloQuery no-prefetch>
```
## On the server
To prefetch all the apollo queries you marked, use the `apolloProvider.prefetchAll` method. The first argument is the context object passed to the `prefetch` hooks (see above). It is recommended to pass the vue-router `currentRoute` object. The second argument is the array of component definition to include (e.g. from `router.getMatchedComponents` method). The third argument is an optional `options` object. It returns a promise resolved when all the apollo queries are loaded.
In the server entry, you need to install `ApolloSSR` plugin into Vue:
```js
import Vue from 'vue'
import ApolloSSR from 'vue-apollo/ssr'
Vue.use(ApolloSSR)
```
To prefetch all the apollo queries you marked, use the `ApolloSSR.prefetchAll` method. The first argument is the `apolloProvider`. The second argument is the array of component definition to include (e.g. from `router.getMatchedComponents` method). The third argument is the context object passed to the `prefetch` hooks (see above). It is recommended to pass the vue-router `currentRoute` object. It returns a promise resolved when all the apollo queries are loaded.
Here is an example with vue-router and a Vuex store:
```js
import Vue from 'vue'
import ApolloSSR from 'vue-apollo/ssr'
Vue.use(ApolloSSR)
export default () => new Promise((resolve, reject) => {
const { app, router, store, apolloProvider } = CreateApp({
ssr: true,
@@ -119,16 +152,16 @@ export default () => new Promise((resolve, reject) => {
// A preFetch hook dispatches a store action and returns a Promise,
// which is resolved when the action is complete and store state has been
// updated.
Promise.all([
// Vuex Store prefetch
...matchedComponents.map(component => {
return component.preFetch && component.preFetch(store)
}),
// Apollo prefetch
apolloProvider.prefetchAll({
route: router.currentRoute,
}, matchedComponents),
]).then(() => {
// Vuex Store prefetch
Promise.all(matchedComponents.map(component => {
return component.preFetch && component.preFetch(store)
})
// Apollo prefetch
.then(() => ApolloSSR.prefetchAll(apolloProvider, matchedComponents, {
route: router.currentRoute,
})
.then(() => {
// Inject the Vuex state and the Apollo cache on the page.
// This will prevent unnecessary queries.
@@ -136,7 +169,7 @@ export default () => new Promise((resolve, reject) => {
js += `window.__INITIAL_STATE__=${JSON.stringify(store.state)};`
// Apollo
js += apolloProvider.exportStates()
js += ApolloSSR.exportStates(apolloProvider)
resolve({
app,
@@ -147,17 +180,7 @@ export default () => new Promise((resolve, reject) => {
})
```
The `options` argument defaults to:
```js
{
// Include components outside of the routes
// that are registered with `willPrefetch`
includeGlobal: true,
}
```
Use the `apolloProvider.exportStates` method to get the JavaScript code you need to inject into the generated page to pass the apollo cache data to the client.
Use the `ApolloSSR.exportStates(apolloProvider, options)` method to get the JavaScript code you need to inject into the generated page to pass the apollo cache data to the client.
It takes an `options` argument which defaults to:
@@ -172,7 +195,7 @@ It takes an `options` argument which defaults to:
}
```
You can also use the `apolloProvider.getStates` method to get the JS object instead of the script string.
You can also use the `ApolloSSR.getStates(apolloProvider, options)` method to get the JS object instead of the script string.
It takes an `options` argument which defaults to:
@@ -242,13 +265,8 @@ Example for common `CreateApp` method:
```js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import Vuex from 'vuex'
Vue.use(Vuex)
import { sync } from 'vuex-router-sync'
import VueApollo from 'vue-apollo'
@@ -258,6 +276,9 @@ import App from './ui/App.vue'
import routes from './routes'
import storeOptions from './store'
Vue.use(VueRouter)
Vue.use(Vuex)
function createApp (context) {
const router = new VueRouter({
mode: 'history',
@@ -281,7 +302,7 @@ function createApp (context) {
el: '#app',
router,
store,
provide: apolloProvider.provide(),
apolloProvider,
...App,
}),
router,
+1 -1
View File
@@ -1,4 +1,4 @@
export const VUE_APOLLO_QUERY_KEYWORDS = [
exports.VUE_APOLLO_QUERY_KEYWORDS = [
'variables',
'watch',
'update',
+10 -8
View File
@@ -1,27 +1,27 @@
import * as TD from 'throttle-debounce'
const TD = require('throttle-debounce')
export const Globals = {}
const Globals = exports.Globals = {}
function factory (action) {
return (cb, time) => action(time, cb)
}
export const throttle = factory(TD.throttle)
exports.throttle = factory(TD.throttle)
export const debounce = factory(TD.debounce)
exports.debounce = factory(TD.debounce)
export function getMergedDefinition (def) {
exports.getMergedDefinition = function (def) {
return Globals.Vue.util.mergeOptions({}, def)
}
export function reapply (options, context) {
exports.reapply = function (options, context) {
while (typeof options === 'function') {
options = options.call(context)
}
return options
}
export function omit (obj, properties) {
exports.omit = function (obj, properties) {
return Object.entries(obj)
.filter(([key]) => !properties.includes(key))
.reduce((c, [key, val]) => {
@@ -30,8 +30,10 @@ export function omit (obj, properties) {
}, {})
}
export function addGqlError (error) {
exports.addGqlError = function (error) {
if (error.graphQLErrors && error.graphQLErrors.length) {
error.gqlError = error.graphQLErrors[0]
}
}
exports.noop = () => {}
+2 -1
View File
@@ -12,7 +12,7 @@
"build:es": "rollup --config build/rollup.config.es.js",
"build:umd": "rollup --config build/rollup.config.umd.js",
"prepublishOnly": "npm run test && npm run build",
"dev": "nodemon --exec 'npm run build:es' --watch src",
"dev": "nodemon --exec 'npm run build:es && npm run build:umd' --watch src",
"test": "npm run test:eslint && npm run test:types",
"test:eslint": "eslint --ext .js src && eslint --ext .js,.vue,.gql tests/demo/src",
"test:types": "tsc -p types/test",
@@ -38,6 +38,7 @@
"apollo-client": "^2.0.0"
},
"dependencies": {
"chalk": "^2.4.1",
"throttle-debounce": "^2.0.0"
},
"devDependencies": {
-177
View File
@@ -1,6 +1,3 @@
import { VUE_APOLLO_QUERY_KEYWORDS } from './consts'
import { getMergedDefinition, omit } from './utils'
export class ApolloProvider {
constructor (options) {
if (!options) {
@@ -21,178 +18,4 @@ export class ApolloProvider {
[key]: this,
}
}
addQueryToPrefetch (queryOptions, client) {
this.prefetchQueries.push({
queryOptions,
client,
})
}
prefetchComponent (component, context) {
component = getMergedDefinition(component)
const apolloOptions = component.apollo
if (!apolloOptions) {
return
}
const componentClient = apolloOptions.$client
for (let key in apolloOptions) {
const options = apolloOptions[key]
if (
key.charAt(0) !== '$' && (
!options.query || (
(typeof options.ssr === 'undefined' || options.ssr) &&
(typeof options.prefetch !== 'undefined' && options.prefetch)
)
)
) {
this.addQueryToPrefetch(options, options.client || componentClient)
}
}
}
prefetchComponents (definitions) {
for (const def of definitions) {
this.prefetchComponent(def)
}
}
prefetchAll (context, components, options) {
// Optional components argument
if (!options && components && !Array.isArray(components)) {
options = components
components = undefined
}
const finalOptions = Object.assign({}, {
includeGlobal: true,
}, options)
if (components) {
this.prefetchComponents(components)
}
if (finalOptions.includeGlobal) {
this.prefetchComponents(globalPrefetchs.filter(
({ component, contextCallback }) => {
let result = true
if (typeof contextCallback === 'function') {
result = !!contextCallback(context)
}
return result
}
).map(
({ component }) => component
), context)
}
return Promise.all(this.prefetchQueries.map(
o => this.prefetchQuery(o.queryOptions, context, o.client)
))
}
prefetchQuery (queryOptions, context, client) {
let variables
// Client
if (!client) {
client = this.defaultClient
} else if (typeof client === 'string') {
client = this.clients[client]
if (!client) {
throw new Error(`[vue-apollo] Missing client '${client}' in 'apolloProvider'`)
}
}
// Simple query
if (!queryOptions.query) {
queryOptions = {
query: queryOptions,
}
} else {
const prefetch = queryOptions.prefetch
const prefetchType = typeof prefetch
// Resolve variables
if (prefetchType !== 'undefined') {
let result
if (prefetchType === 'function') {
result = prefetch(context)
} else {
result = prefetch
}
if (!result) {
return Promise.resolve()
} else if (prefetchType === 'boolean') {
const optVariables = queryOptions.variables
if (typeof optVariables !== 'undefined') {
// Reuse `variables` option with `prefetch: true`
if (typeof optVariables === 'function') {
variables = optVariables.call(context)
} else {
variables = optVariables
}
} else {
variables = undefined
}
} else {
variables = result
}
}
}
// Query
if (typeof queryOptions.query === 'function') {
queryOptions.query = queryOptions.query(context)
}
return new Promise((resolve, reject) => {
const options = omit(queryOptions, [
...VUE_APOLLO_QUERY_KEYWORDS,
'fetchPolicy',
])
options.variables = variables
options.fetchPolicy = 'network-only'
client.query(options).then(resolve, reject)
})
}
getStates (options) {
const finalOptions = Object.assign({}, {
exportNamespace: '',
}, options)
const states = {}
for (const key in this.clients) {
const client = this.clients[key]
const state = client.cache.extract()
states[`${finalOptions.exportNamespace}${key}`] = state
}
return states
}
exportStates (options) {
const finalOptions = Object.assign({}, {
globalName: '__APOLLO_STATE__',
attachTo: 'window',
}, options)
const states = this.getStates(finalOptions)
const js = `${finalOptions.attachTo}.${finalOptions.globalName} = ${JSON.stringify(states)};`
return js
}
}
const globalPrefetchs = []
export function willPrefetch (component, contextCallback = null) {
globalPrefetchs.push({ component, contextCallback })
return component
}
// Global access for libraries
if (typeof window !== 'undefined') {
window.vueApolloWillPrefetch = willPrefetch
} else if (typeof global !== 'undefined') {
global.vueApolloWillPrefetch = willPrefetch
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { addGqlError } from '../utils'
import { addGqlError } from '../../lib/utils'
export default {
props: {
+1 -1
View File
@@ -1,6 +1,6 @@
import SmartQuery from './smart-query'
import SmartSubscription from './smart-subscription'
import { reapply } from './utils'
import { reapply } from '../lib/utils'
export class DollarApollo {
constructor (vm) {
+1 -2
View File
@@ -6,7 +6,7 @@ import CApolloSubscribeToMore from './components/ApolloSubscribeToMore'
import CApolloMutation from './components/ApolloMutation'
import { installMixin } from './mixin'
import { Globals, omit } from './utils'
import { Globals, omit } from '../lib/utils'
const keywords = [
'$subscribe',
@@ -66,7 +66,6 @@ plugin.version = VERSION
// Apollo provider
export const ApolloProvider = plugin
export { willPrefetch } from './apollo-provider'
// Components
export const ApolloQuery = CApolloQuery
+1 -1
View File
@@ -1,4 +1,4 @@
import { Globals } from './utils'
import { Globals } from '../lib/utils'
function hasProperty (holder, key) {
return typeof holder !== 'undefined' && Object.prototype.hasOwnProperty.call(holder, key)
+1 -1
View File
@@ -1,4 +1,4 @@
import { throttle, debounce, omit, addGqlError } from './utils'
import { throttle, debounce, omit, addGqlError } from '../lib/utils'
export default class SmartApollo {
type = null
+1 -1
View File
@@ -1,5 +1,5 @@
import SmartApollo from './smart-apollo'
import { VUE_APOLLO_QUERY_KEYWORDS } from './consts'
import { VUE_APOLLO_QUERY_KEYWORDS } from '../lib/consts'
export default class SmartQuery extends SmartApollo {
type = 'query'
+29
View File
@@ -0,0 +1,29 @@
exports.VM_HELPERS = [
'_ssrAttr',
'_ssrNode',
'_ssrEscape',
'_ssrList',
'_o',
'_n',
'_s',
'_l',
'_t',
'_q',
'_i',
'_m',
'_f',
'_k',
'_b',
'_v',
'_e',
'_g',
]
exports.COMPONENT_BLACKLIST = [
'router-link',
'router-view',
'router-multi-view',
'apollo-mutation',
'apollo-subscribe',
'apollo-subscribe-to-more',
]
+305
View File
@@ -0,0 +1,305 @@
const chalk = require('chalk')
const { VUE_APOLLO_QUERY_KEYWORDS } = require('../lib/consts')
const { VM_HELPERS, COMPONENT_BLACKLIST } = require('./consts')
const { Globals, getMergedDefinition, omit, noop } = require('../lib/utils')
exports.install = function (Vue) {
Globals.Vue = Vue
}
exports.prefetchAll = function (apolloProvider, components, context) {
return exports.getQueriesFromTree(components, context)
.then(queries => Promise.all(queries.map(
query => prefetchQuery(apolloProvider, query, context)
)))
}
exports.getQueriesFromTree = function (components, context) {
const queries = []
return Promise.all(
components.map(component => walkTree(component, {}, {}, [], context, queries, components))
).then(() => queries)
}
function walkTree (component, data, parent, children, context, queries, components) {
component = getMergedDefinition(component)
return new Promise((resolve, reject) => {
const queue = []
const vm = createFakeInstance(component, data, parent, children, context)
vm.$createElement = (el, data, children) => {
if (typeof data === 'string' || Array.isArray(data)) {
children = data
data = {}
}
// No Prefetch flag
if (data && data.attrs && data.attrs['no-prefetch']) return
queue.push(resolveComponent(el, component).then(resolvedComponent => {
let child
if (resolvedComponent && !components.includes(resolvedComponent)) {
child = {
component: resolvedComponent,
data,
children,
}
}
return child
}))
}
prefetchComponent(component, vm, queries)
try {
component.render.call(vm, vm.$createElement)
} catch (e) {
console.log(chalk.red(`Error while rendering ${component.name || component.__file}`))
console.log(e.stack)
}
Promise.all(queue).then(queue => queue.filter(child => !!child).map(
child => walkTree(child.component, child.data, vm, child.children, context, queries, components)
)).then(() => resolve())
})
}
function prefetchComponent (component, vm, queries) {
const apolloOptions = component.apollo
if (!apolloOptions) return
if (apolloOptions.$prefetch === false) return
const componentClient = apolloOptions.$client
for (let key in apolloOptions) {
const options = apolloOptions[key]
if (
key.charAt(0) !== '$' && (
!options.query || (
(typeof options.ssr === 'undefined' || options.ssr) &&
(typeof options.prefetch !== 'undefined' && options.prefetch)
)
)
) {
queries.push({
queryOptions: options,
client: options.client || componentClient,
vm,
})
}
}
}
function createFakeInstance (options, data, parent, children, context) {
const vm = {
...data.attrs,
...data.props,
$parent: parent,
$children: children,
$slots: {},
$scopedSlots: {},
$set: Globals.Vue.set,
$delete: Globals.Vue.delete,
$route: context.route,
$store: context.store,
$apollo: {
queries: {},
loading: false,
},
$apolloData: {
loading: false,
},
_self: {},
_staticTrees: [],
_u: resolveScopedSlots,
}
// Render and other helpers
VM_HELPERS.forEach(helper => vm[helper] = noop)
// Scoped slots
if (data.scopedSlots) {
vm.$scopedSlots = data.scopedSlots
}
// Route props
if (context && context.route) {
const { route } = context
const matchedRoute = findRouteMatch(options, route)
if (matchedRoute && matchedRoute.props) {
const { props } = matchedRoute
if (props === true) {
Object.assign(vm, matchedRoute.params)
} else if (typeof props === 'function') {
Object.assign(vm, props(matchedRoute))
} else if (typeof props === 'object') {
Object.assign(vm, props)
}
}
}
// Data
const localData = options.data
if (typeof localData === 'function') {
Object.assign(vm, localData(vm))
} else if (typeof localData === 'object') {
Object.assign(vm, localData)
}
// Prefetch state
const prefetch = options.prefetch
if (typeof prefetch === 'function') {
Object.assign(vm, prefetch(context))
} else if (typeof prefetch === 'object') {
Object.assign(vm, prefetch)
}
return vm
}
function findRouteMatch (component, route) {
for (const r of route.matched) {
for (const key in r.components) {
if (r.components[key] === component) {
return r
}
}
}
}
function resolveComponent (name, options) {
return new Promise((resolve) => {
if (options.components) {
if (options.components[name]) {
return resolve(options.components[name])
}
}
return resolve(Globals.Vue.options.components[name])
}).then(component => {
if (component) {
component = getMergedDefinition(component)
if (!component.functional && (
!component.name ||
!COMPONENT_BLACKLIST.includes(component.name)
)) {
return component
}
}
})
}
function resolveScopedSlots (fns, res) {
res = res || {}
for (var i = 0; i < fns.length; i++) {
if (Array.isArray(fns[i])) {
resolveScopedSlots(fns[i], res)
} else {
res[fns[i].key] = fns[i].fn
}
}
return res
}
function prefetchQuery (apolloProvider, query, context) {
try {
let variables
let { queryOptions, client, vm } = query
// Client
if (typeof client === 'function') {
client = client.call(vm)
}
if (!client) {
client = apolloProvider.defaultClient
} else if (typeof client === 'string') {
client = apolloProvider.clients[client]
if (!client) {
throw new Error(`[vue-apollo] Missing client '${client}' in 'apolloProvider'`)
}
}
// Function query
if (typeof queryOptions === 'function') {
queryOptions = queryOptions.call(vm)
}
// Simple query
if (!queryOptions.query) {
queryOptions = {
query: queryOptions,
}
} else {
const prefetch = queryOptions.prefetch
const prefetchType = typeof prefetch
// Resolve variables
let prefetchResult
if (prefetchType !== 'undefined') {
if (prefetchType === 'function') {
prefetchResult = prefetch.call(vm, context)
} else {
prefetchResult = prefetch
}
if (prefetchType === 'boolean' && prefetchResult === false) {
return Promise.resolve()
}
}
if (prefetchResult) {
variables = prefetchResult
} else {
const optVariables = queryOptions.variables
if (typeof optVariables !== 'undefined') {
// Reuse `variables` option with `prefetch: true`
if (typeof optVariables === 'function') {
variables = optVariables.call(vm)
} else {
variables = optVariables
}
} else {
variables = undefined
}
}
}
// Query
if (typeof queryOptions.query === 'function') {
queryOptions.query = queryOptions.query.call(vm)
}
const options = omit(queryOptions, [
...VUE_APOLLO_QUERY_KEYWORDS,
'fetchPolicy',
])
options.variables = variables
options.fetchPolicy = 'network-only'
return client.query(options)
} catch (e) {
console.log(e.stack)
}
}
exports.getStates = function (apolloProvider, options) {
const finalOptions = Object.assign({}, {
exportNamespace: '',
}, options)
const states = {}
for (const key in apolloProvider.clients) {
const client = apolloProvider.clients[key]
const state = client.cache.extract()
states[`${finalOptions.exportNamespace}${key}`] = state
}
return states
}
exports.exportStates = function (apolloProvider, options) {
const finalOptions = Object.assign({}, {
globalName: '__APOLLO_STATE__',
attachTo: 'window',
}, options)
const states = exports.getStates(apolloProvider, finalOptions)
const js = `${finalOptions.attachTo}.${finalOptions.globalName} = ${JSON.stringify(states)};`
return js
}