mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-12 22:32:27 +03:00
feat: convert to ts (wip)
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"@nuxtjs"
|
||||
],
|
||||
"globals": {
|
||||
"Vue": "readable"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
node_modules
|
||||
__build__
|
||||
dist
|
||||
|
||||
.vue-meta
|
||||
_old
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3,
|
||||
targets: {
|
||||
ie: 9
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"dynamic-import-node"
|
||||
]
|
||||
}
|
||||
@@ -10,14 +10,14 @@ window.users.push({
|
||||
zipcode: '92998-3874',
|
||||
geo: {
|
||||
lat: '-37.3159',
|
||||
lng: '81.1496'
|
||||
}
|
||||
lng: '81.1496',
|
||||
},
|
||||
},
|
||||
phone: '1-770-736-8031 x56442',
|
||||
website: 'hildegard.org',
|
||||
company: {
|
||||
name: 'Romaguera-Crona',
|
||||
catchPhrase: 'Multi-layered client-server neural-net',
|
||||
bs: 'harness real-time e-markets'
|
||||
}
|
||||
bs: 'harness real-time e-markets',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,14 +10,14 @@ window.users.push({
|
||||
zipcode: '90566-7771',
|
||||
geo: {
|
||||
lat: '-43.9509',
|
||||
lng: '-34.4618'
|
||||
}
|
||||
lng: '-34.4618',
|
||||
},
|
||||
},
|
||||
phone: '010-692-6593 x09125',
|
||||
website: 'anastasia.net',
|
||||
company: {
|
||||
name: 'Deckow-Crist',
|
||||
catchPhrase: 'Proactive didactic contingency',
|
||||
bs: 'synergize scalable supply-chains'
|
||||
}
|
||||
bs: 'synergize scalable supply-chains',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,14 +10,14 @@ window.users.push({
|
||||
zipcode: '59590-4157',
|
||||
geo: {
|
||||
lat: '-68.6102',
|
||||
lng: '-47.0653'
|
||||
}
|
||||
lng: '-47.0653',
|
||||
},
|
||||
},
|
||||
phone: '1-463-123-4447',
|
||||
website: 'ramiro.info',
|
||||
company: {
|
||||
name: 'Romaguera-Jacobson',
|
||||
catchPhrase: 'Face to face bifurcated interface',
|
||||
bs: 'e-enable strategic applications'
|
||||
}
|
||||
bs: 'e-enable strategic applications',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,14 +10,14 @@ window.users.push({
|
||||
zipcode: '53919-4257',
|
||||
geo: {
|
||||
lat: '29.4572',
|
||||
lng: '-164.2990'
|
||||
}
|
||||
lng: '-164.2990',
|
||||
},
|
||||
},
|
||||
phone: '493-170-9623 x156',
|
||||
website: 'kale.biz',
|
||||
company: {
|
||||
name: 'Robel-Corkery',
|
||||
catchPhrase: 'Multi-tiered zero tolerance productivity',
|
||||
bs: 'transition cutting-edge web services'
|
||||
}
|
||||
bs: 'transition cutting-edge web services',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ Vue.use(VueMeta)
|
||||
window.users = []
|
||||
|
||||
new Vue({
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: 'Async Callback',
|
||||
titleTemplate: '%s | Vue Meta Examples',
|
||||
@@ -16,56 +16,56 @@ new Vue({
|
||||
vmid: 'potatoes',
|
||||
src: '/user-3.js',
|
||||
async: true,
|
||||
callback: this.updateCounter
|
||||
callback: this.updateCounter,
|
||||
},
|
||||
{
|
||||
skip: this.count < 1,
|
||||
vmid: 'vegetables',
|
||||
src: '/user-2.js',
|
||||
async: true,
|
||||
callback: this.updateCounter
|
||||
callback: this.updateCounter,
|
||||
},
|
||||
{
|
||||
vmid: 'meat',
|
||||
src: '/user-1.js',
|
||||
async: true,
|
||||
callback: el => this.loadCallback(el.getAttribute('data-vmid'))
|
||||
callback: el => this.loadCallback(el.getAttribute('data-vmid')),
|
||||
},
|
||||
...this.scripts
|
||||
]
|
||||
...this.scripts,
|
||||
],
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
count: 0,
|
||||
scripts: [],
|
||||
users: window.users
|
||||
users: window.users,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
count (val) {
|
||||
count(val) {
|
||||
if (val === 3) {
|
||||
this.addScript()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateCounter () {
|
||||
updateCounter() {
|
||||
this.count++
|
||||
},
|
||||
addScript () {
|
||||
addScript() {
|
||||
this.scripts.push({
|
||||
src: '/user-4.js',
|
||||
callback: () => {
|
||||
this.updateCounter()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
loadCallback (vmid) {
|
||||
loadCallback(vmid) {
|
||||
if (vmid === 'meat') {
|
||||
this.updateCounter()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div id="app">
|
||||
@@ -84,5 +84,5 @@ new Vue({
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
}).$mount('#app')
|
||||
|
||||
@@ -8,17 +8,17 @@ Vue.component('child', {
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
render (h) {
|
||||
render(h) {
|
||||
return h('h3', null, this.page)
|
||||
},
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.page
|
||||
title: this.page,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
new Vue({
|
||||
@@ -28,5 +28,5 @@ new Vue({
|
||||
<p>Inspect Element to see the meta info</p>
|
||||
<child page="This is a prop"></child>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
}).$mount('#app')
|
||||
|
||||
+15
-9
@@ -15,18 +15,24 @@ new Vue({
|
||||
titleTemplate: '%s | Vue Meta Examples',
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
amp: undefined
|
||||
amp: undefined,
|
||||
},
|
||||
headAttrs: {
|
||||
test: true
|
||||
test: true,
|
||||
},
|
||||
meta: [
|
||||
{ name: 'description', content: 'Hello', vmid: 'test' }
|
||||
],
|
||||
meta: [{ name: 'description', content: 'Hello', vmid: 'test' }],
|
||||
script: [
|
||||
{ innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }', type: 'application/ld+json' },
|
||||
{ innerHTML: '{ "body": "yes" }', body: true, type: 'application/ld+json' }
|
||||
{
|
||||
innerHTML:
|
||||
'{ "@context": "http://www.schema.org", "@type": "Organization" }',
|
||||
type: 'application/ld+json',
|
||||
},
|
||||
{
|
||||
innerHTML: '{ "body": "yes" }',
|
||||
body: true,
|
||||
type: 'application/ld+json',
|
||||
},
|
||||
],
|
||||
__dangerouslyDisableSanitizers: ['script']
|
||||
})
|
||||
__dangerouslyDisableSanitizers: ['script'],
|
||||
}),
|
||||
}).$mount('#app')
|
||||
|
||||
@@ -6,18 +6,18 @@ Vue.use(VueMeta)
|
||||
Vue.component('foo', {
|
||||
template: '<p>Foo component</p>',
|
||||
metaInfo: {
|
||||
title: 'Keep me Foo'
|
||||
}
|
||||
title: 'Keep me Foo',
|
||||
},
|
||||
})
|
||||
|
||||
new Vue({
|
||||
data () {
|
||||
data() {
|
||||
return { showFoo: false }
|
||||
},
|
||||
methods: {
|
||||
show () {
|
||||
show() {
|
||||
this.showFoo = !this.showFoo
|
||||
}
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div id="app">
|
||||
@@ -29,6 +29,6 @@ new Vue({
|
||||
</div>
|
||||
`,
|
||||
metaInfo: () => ({
|
||||
title: 'Keep-alive'
|
||||
})
|
||||
title: 'Keep-alive',
|
||||
}),
|
||||
}).$mount('#app')
|
||||
|
||||
+11
-10
@@ -5,12 +5,12 @@ const {
|
||||
processIf,
|
||||
getBaseTransformPreset,
|
||||
createObjectExpression,
|
||||
createObjectProperty
|
||||
createObjectProperty,
|
||||
} = require('@vue/compiler-core')
|
||||
|
||||
const { parse } = require('@vue/compiler-dom')
|
||||
|
||||
function headTransform (node, context) {
|
||||
function headTransform(node, context) {
|
||||
console.log('NODE', node)
|
||||
if (node.type === 1 /* NodeTypes.ELEMENT */) {
|
||||
return () => {
|
||||
@@ -35,23 +35,22 @@ module.exports = function (source, map) {
|
||||
// console.log('AST', ast)
|
||||
|
||||
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset({
|
||||
prefixIdentifiers: true
|
||||
prefixIdentifiers: true,
|
||||
})
|
||||
|
||||
transform(ast, {
|
||||
prefixIdentifiers: true,
|
||||
nodeTransforms: [
|
||||
...nodeTransforms,
|
||||
headTransform
|
||||
],
|
||||
directiveTransforms
|
||||
nodeTransforms: [...nodeTransforms, headTransform],
|
||||
directiveTransforms,
|
||||
})
|
||||
|
||||
const result = generate(ast, { mode: 'module' })
|
||||
|
||||
console.log(result.code)
|
||||
|
||||
this.callback(null, `
|
||||
this.callback(
|
||||
null,
|
||||
`
|
||||
import { computed } from 'vue'
|
||||
|
||||
${result.code}
|
||||
@@ -70,6 +69,8 @@ export default function (component) {
|
||||
}
|
||||
}
|
||||
|
||||
}`, map)
|
||||
}`,
|
||||
map
|
||||
)
|
||||
/**/
|
||||
}
|
||||
|
||||
@@ -6,50 +6,50 @@ Vue.use(VueMeta)
|
||||
// index.html contains a manual SSR render
|
||||
|
||||
const app1 = new Vue({
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: 'App 1 title',
|
||||
bodyAttrs: {
|
||||
class: 'app-1'
|
||||
class: 'app-1',
|
||||
},
|
||||
meta: [
|
||||
{ name: 'description', content: 'Hello from app 1', vmid: 'test' },
|
||||
{ name: 'og:description', content: this.ogContent }
|
||||
{ name: 'og:description', content: this.ogContent },
|
||||
],
|
||||
script: [
|
||||
{ innerHTML: 'var appId=1.1', body: true },
|
||||
{ innerHTML: 'var appId=1.2', vmid: 'app-id-body' }
|
||||
]
|
||||
{ innerHTML: 'var appId=1.2', vmid: 'app-id-body' },
|
||||
],
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
ogContent: 'Hello from ssr app'
|
||||
ogContent: 'Hello from ssr app',
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div id="app1"><h1>App 1</h1></div>
|
||||
`
|
||||
`,
|
||||
})
|
||||
|
||||
const app2 = new Vue({
|
||||
metaInfo: () => ({
|
||||
title: 'App 2 title',
|
||||
bodyAttrs: {
|
||||
class: 'app-2'
|
||||
class: 'app-2',
|
||||
},
|
||||
meta: [
|
||||
{ name: 'description', content: 'Hello from app 2', vmid: 'test' },
|
||||
{ name: 'og:description', content: 'Hello from app 2' }
|
||||
{ name: 'og:description', content: 'Hello from app 2' },
|
||||
],
|
||||
script: [
|
||||
{ innerHTML: 'var appId=2.1', body: true },
|
||||
{ innerHTML: 'var appId=2.2', vmid: 'app-id-body', body: true }
|
||||
]
|
||||
{ innerHTML: 'var appId=2.2', vmid: 'app-id-body', body: true },
|
||||
],
|
||||
}),
|
||||
template: `
|
||||
<div id="app2"><h1>App 2</h1></div>
|
||||
`
|
||||
`,
|
||||
}).$mount('#app2')
|
||||
|
||||
app1.$mount('#app1')
|
||||
@@ -57,7 +57,7 @@ app1.$mount('#app1')
|
||||
const app3 = new Vue({
|
||||
template: `
|
||||
<div id="app3"><h1>App 3 (empty metaInfo)</h1></div>
|
||||
`
|
||||
`,
|
||||
}).$mount('#app3')
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<script>
|
||||
import { h, ref, computed, inject, Teleport } from 'vue'
|
||||
import { renderMeta } from './render'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
metainfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
},
|
||||
render() {
|
||||
const targets = {}
|
||||
|
||||
for (const key in this.metainfo) {
|
||||
const config = this.$metaInfo.config[key] || {}
|
||||
|
||||
const vnodes = renderMeta(this, key, this.metainfo[key], config)
|
||||
let target = (key !== 'base' && this.metainfo[key].target) || config.target || 'head'
|
||||
|
||||
if (Array.isArray(vnodes)) {
|
||||
for (const vnode of vnodes) {
|
||||
if (vnode.__vm_target) {
|
||||
target = vnode.__vm_target
|
||||
delete vnode.__vm_target
|
||||
}
|
||||
|
||||
if (!targets[target]) {
|
||||
targets[target] = []
|
||||
}
|
||||
|
||||
targets[target].push(vnode)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!targets[target]) {
|
||||
targets[target] = []
|
||||
}
|
||||
|
||||
targets[target].push(vnodes)
|
||||
continue
|
||||
}
|
||||
// console.log('TARGETS', targets)
|
||||
return Object.keys(targets).map(target => {
|
||||
return h(Teleport, { to: target }, targets[target])
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,119 +0,0 @@
|
||||
const defaults = {
|
||||
title: {
|
||||
contentAttributes: false
|
||||
},
|
||||
base: {
|
||||
contentAttributes: [
|
||||
'href',
|
||||
'target'
|
||||
]
|
||||
},
|
||||
meta: {
|
||||
nameAttribute: 'name',
|
||||
contentAttributes: [
|
||||
'content',
|
||||
'name',
|
||||
'http-equiv',
|
||||
'charset'
|
||||
]
|
||||
},
|
||||
link: {
|
||||
contentAttributes: [
|
||||
'href',
|
||||
'crossorigin',
|
||||
'rel',
|
||||
'media',
|
||||
'integrity',
|
||||
'hreflang',
|
||||
'type',
|
||||
'referrerpolicy',
|
||||
'sizes',
|
||||
'imagesrcset',
|
||||
'imagesizes',
|
||||
'as',
|
||||
'color'
|
||||
]
|
||||
},
|
||||
style: {
|
||||
contentAttributes: [
|
||||
'media'
|
||||
]
|
||||
},
|
||||
script: {
|
||||
contentAttributes: [
|
||||
'src',
|
||||
'type',
|
||||
'nomodule',
|
||||
'async',
|
||||
'defer',
|
||||
'crossorigin',
|
||||
'integrity',
|
||||
'referrerpolicy'
|
||||
]
|
||||
},
|
||||
noscript: {
|
||||
contentAttributes: false
|
||||
}
|
||||
}
|
||||
|
||||
const defaultMapping = {
|
||||
body: {
|
||||
tag: 'script',
|
||||
target: 'body'
|
||||
},
|
||||
base: {
|
||||
contentAttribute: 'href'
|
||||
},
|
||||
charset: {
|
||||
tag: 'meta',
|
||||
nameless: true,
|
||||
contentAttribute: 'charset'
|
||||
},
|
||||
description: {
|
||||
tag: 'meta'
|
||||
},
|
||||
og: {
|
||||
group: true,
|
||||
namespacedAttribute: true,
|
||||
tag: 'meta',
|
||||
nameAttribute: 'property'
|
||||
},
|
||||
twitter: {
|
||||
group: true,
|
||||
namespacedAttribute: true,
|
||||
tag: 'meta'
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
defaults,
|
||||
defaultMapping
|
||||
}
|
||||
|
||||
export function hasConfig (name) {
|
||||
return !!defaults[name] || !!defaultMapping[name]
|
||||
}
|
||||
|
||||
export function getConfigKey (name, key, config, dontLog) {
|
||||
if (!dontLog) {
|
||||
// console.log('getConfigKey', name, key, getConfigKey(name, key, config, true), config)
|
||||
}
|
||||
|
||||
if (config && key in config) {
|
||||
return config[key]
|
||||
}
|
||||
|
||||
if (Array.isArray(name)) {
|
||||
for (const _name of name) {
|
||||
if (_name && _name in defaults) {
|
||||
return defaults[_name][key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (name in defaults) {
|
||||
return defaults[name][key]
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import { markRaw, reactive, onUnmounted, getCurrentInstance } from 'vue'
|
||||
import { hasOwn, isObject, isArray, isPlainObject } from '@vue/shared'
|
||||
import { defaultMapping } from './config'
|
||||
|
||||
let appId = 0
|
||||
|
||||
const shadow = markRaw({})
|
||||
const metainfo = reactive({})
|
||||
|
||||
export function createMeta ({ resolver, config }) {
|
||||
const id = Symbol(`vueMeta[${appId++}]`)
|
||||
|
||||
return {
|
||||
install (app) {
|
||||
const $metaInfo = {
|
||||
id,
|
||||
resolver,
|
||||
config: {
|
||||
...defaultMapping,
|
||||
...config
|
||||
}
|
||||
}
|
||||
|
||||
app.config.globalProperties.$metaInfo = $metaInfo
|
||||
app.provide('metainfo', metainfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useMeta (obj, context) {
|
||||
// set empty object to remove everything
|
||||
const unmount = () => setMetainfoByObject(context, {}, shadow, metainfo)
|
||||
|
||||
if (!context) {
|
||||
context = getCurrentInstance()
|
||||
|
||||
onUnmounted(unmount)
|
||||
}
|
||||
|
||||
if (!context) {
|
||||
context = {}
|
||||
}
|
||||
|
||||
setMetainfoByObject(context, obj, shadow, metainfo)
|
||||
|
||||
const meta = createProxy(obj, createHandler(context))
|
||||
|
||||
return {
|
||||
meta,
|
||||
unmount
|
||||
}
|
||||
}
|
||||
|
||||
function createProxy (obj, handler) {
|
||||
return markRaw(new Proxy(obj, handler))
|
||||
}
|
||||
|
||||
function createHandler (context, keyPath = []) {
|
||||
return {
|
||||
get (target, prop) {
|
||||
const value = target[prop]
|
||||
|
||||
if (!isObject(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (!value.__vm_proxy) {
|
||||
const newKeyPath = [...keyPath, prop]
|
||||
|
||||
const handler = createHandler(context, newKeyPath)
|
||||
value.__vm_proxy = createProxy(value, handler)
|
||||
}
|
||||
|
||||
return value.__vm_proxy
|
||||
},
|
||||
set (target, prop, value) {
|
||||
updateMetainfo(keyPath, context, prop, value)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setMetainfo (context, key, value, shadowParent, liveParent, keyTree = []) {
|
||||
if (isPlainObject(value)) {
|
||||
if (!shadowParent[key]) {
|
||||
shadowParent[key] = {}
|
||||
liveParent[key] = {}
|
||||
}
|
||||
|
||||
return setMetainfoByObject(context, value, shadowParent[key], liveParent[key], keyTree)
|
||||
}
|
||||
|
||||
let idx = -1
|
||||
if (!hasOwn(shadowParent, key)) {
|
||||
shadowParent[key] = []
|
||||
} else {
|
||||
idx = shadowParent[key].findIndex(({ context: $context }) => $context === context)
|
||||
}
|
||||
|
||||
if (idx > -1 && value === undefined) {
|
||||
shadowParent[key].splice(idx, 1)
|
||||
} else if (idx > -1) {
|
||||
shadowParent[key][idx].value = value
|
||||
} else if (value) {
|
||||
shadowParent[key].push({ context, value })
|
||||
}
|
||||
|
||||
resolveActiveMetainfo(context, key, keyTree, shadowParent, liveParent)
|
||||
}
|
||||
|
||||
function setMetainfoByObject (context, obj, shadowParent, liveParent, keyTree = []) {
|
||||
for (const key in shadowParent) {
|
||||
if (hasOwn(obj, key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (isPlainObject(shadowParent[key])) {
|
||||
setMetainfoByObject(context, {}, shadowParent[key], liveParent[key], [...keyTree, key])
|
||||
continue
|
||||
}
|
||||
|
||||
setMetainfo(context, key, undefined, shadowParent, liveParent, [...keyTree, key])
|
||||
}
|
||||
|
||||
for (const key in obj) {
|
||||
setMetainfo(context, key, obj[key], shadowParent, liveParent, [...keyTree, key])
|
||||
}
|
||||
}
|
||||
|
||||
function updateMetainfo (keyPath, context, key, value) {
|
||||
let shadowParent = shadow
|
||||
let liveParent = metainfo
|
||||
|
||||
for (const _key of keyPath) {
|
||||
shadowParent = shadowParent[_key]
|
||||
liveParent = liveParent[_key]
|
||||
}
|
||||
|
||||
setMetainfo(context, key, value, shadowParent, liveParent)
|
||||
}
|
||||
|
||||
function resolveActiveMetainfo (context, key, keyTree, shadowParent, liveParent) {
|
||||
let value
|
||||
|
||||
if (shadowParent[key].length > 1) {
|
||||
value = context.ctx.$metaInfo.resolver(key, shadowParent[key], liveParent[key])
|
||||
} else if (shadowParent[key].length) {
|
||||
value = shadowParent[key][0].value
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
delete liveParent[key]
|
||||
} else if (!hasOwn(liveParent, key) || liveParent[key] !== value) {
|
||||
liveParent[key] = value
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { h } from 'vue'
|
||||
import { getConfigKey } from './config'
|
||||
|
||||
export function renderMeta (ctx, key, data, config) {
|
||||
// console.info('renderMeta', key, data, config)
|
||||
|
||||
if (config.group) {
|
||||
return renderGroup(ctx, key, data, config)
|
||||
}
|
||||
|
||||
return renderTag(ctx, key, data, config)
|
||||
}
|
||||
|
||||
export function renderGroup (ctx, key, data, config) {
|
||||
// console.info('renderGroup', key, data, config)
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
config.contentAttributes = getConfigKey([key, config.tag], 'contentAttributes', config)
|
||||
return data.map(_data => renderTag(ctx, key, _data, config))
|
||||
}
|
||||
|
||||
return Object.keys(data).map((childKey) => {
|
||||
const groupConfig = {
|
||||
group: key,
|
||||
data
|
||||
}
|
||||
|
||||
if (config.namespaced || config.namespacedAttribute) {
|
||||
let namespace
|
||||
if (config.namespaced) {
|
||||
namespace = config.namespaced === true ? key : config.namespaced
|
||||
groupConfig.tagNamespace = namespace
|
||||
} else {
|
||||
namespace = config.namespacedAttribute === true ? key : config.namespacedAttribute
|
||||
groupConfig.fullName = `${namespace}:${childKey}`
|
||||
groupConfig.slotName = `${namespace}(${childKey})`
|
||||
}
|
||||
}
|
||||
|
||||
return renderTag(ctx, key, data[childKey], config, groupConfig)
|
||||
})
|
||||
}
|
||||
|
||||
export function renderTag (ctx, key, data, config = {}, groupConfig = {}) {
|
||||
if (!config.group && Array.isArray(data)) {
|
||||
return renderTag(ctx, key, { content: data }, config, groupConfig)
|
||||
}
|
||||
|
||||
const { tag = config.tag || key } = data
|
||||
const {
|
||||
slotName = key,
|
||||
fullName = key
|
||||
} = groupConfig
|
||||
|
||||
let content, hasChilds
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((child) => {
|
||||
return renderTag(ctx, key, child, config, groupConfig)
|
||||
})
|
||||
} else if (data.content && Array.isArray(data.content)) {
|
||||
content = data.content.map((child) => {
|
||||
if (typeof child === 'string') {
|
||||
return child
|
||||
}
|
||||
return renderTag(ctx, key, child, config, groupConfig)
|
||||
})
|
||||
hasChilds = true
|
||||
} else {
|
||||
content = data
|
||||
}
|
||||
|
||||
let { attrs: attributes } = data
|
||||
if (!attributes && typeof data === 'object') {
|
||||
attributes = {
|
||||
...data
|
||||
}
|
||||
delete attributes.tag
|
||||
delete attributes.content
|
||||
delete attributes.target
|
||||
} else {
|
||||
attributes = {}
|
||||
}
|
||||
|
||||
if (hasChilds) {
|
||||
content = getSlotContent(ctx, slotName, content, config, data)
|
||||
} else {
|
||||
const contentAttributes = getConfigKey(tag, 'contentAttributes', config)
|
||||
|
||||
if (contentAttributes) {
|
||||
if (!config.nameless) {
|
||||
const nameAttribute = getConfigKey(tag, 'nameAttribute', config)
|
||||
if (nameAttribute) {
|
||||
attributes[nameAttribute] = fullName
|
||||
}
|
||||
}
|
||||
|
||||
const contentAttribute = config.contentAttribute || contentAttributes[0]
|
||||
attributes[contentAttribute] = getSlotContent(ctx, slotName, attributes[contentAttribute] || content, config, groupConfig)
|
||||
content = undefined
|
||||
} else {
|
||||
content = getSlotContent(ctx, slotName, content, config, data, true)
|
||||
}
|
||||
}
|
||||
|
||||
const finalTag = groupConfig.tagNamespace
|
||||
? `${groupConfig.tagNamespace}:${tag}`
|
||||
: tag
|
||||
|
||||
// console.info('FINAL TAG', finalTag)
|
||||
// console.log(' ATTRIBUTES', attributes)
|
||||
// console.log(' CONTENT', content)
|
||||
// // console.log(data, attributes, config)
|
||||
|
||||
if (hasChilds) {
|
||||
for (const child of content) {
|
||||
if (typeof child === 'string') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (child.type === finalTag) {
|
||||
return content
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const vnode = h(finalTag, attributes, content)
|
||||
|
||||
if (data.target) {
|
||||
vnode.__vm_target = data.target
|
||||
}
|
||||
|
||||
return vnode
|
||||
}
|
||||
|
||||
export function getSlotContent ({ metainfo, $slots }, slotName, content, config, groupConfig) {
|
||||
if (!$slots[slotName]) {
|
||||
return content
|
||||
}
|
||||
|
||||
const slotProps = {
|
||||
content,
|
||||
metainfo
|
||||
}
|
||||
|
||||
if (groupConfig.group) {
|
||||
slotProps[groupConfig.group] = groupConfig.data
|
||||
}
|
||||
|
||||
content = $slots[slotName](slotProps)
|
||||
return content[0].children
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "vue-meta-examples",
|
||||
"version": "1.0.0",
|
||||
"description": "Examples for vue-meta",
|
||||
"main": "server.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development babel-node server.js",
|
||||
"start": "babel-node server.js",
|
||||
"ssr": "cross-env NODE_ENV=development babel-node ssr"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/vue-meta.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/nuxt/vue-meta/issues"
|
||||
},
|
||||
"homepage": "https://github.com/nuxt/vue-meta#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/node": "^7.8.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@vue/compiler-sfc": "^3.0.0-alpha.10",
|
||||
"@vue/server-renderer": "^3.0.0-alpha.10",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"consola": "^2.12.1",
|
||||
"core-js": "3",
|
||||
"cross-env": "^7.0.2",
|
||||
"express": "^4.17.1",
|
||||
"express-urlrewrite": "^1.2.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"vue": "next",
|
||||
"vue-loader": "next",
|
||||
"vue-meta": "^2.3.3",
|
||||
"vue-router": "next",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuex": "^3.4.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpackbar": "^4.0.0"
|
||||
}
|
||||
}
|
||||
+11
-9
@@ -10,18 +10,20 @@ import { renderPage } from './ssr/server'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(webpackDevMiddleware(webpack(WebpackConfig), {
|
||||
publicPath: '/__build__/',
|
||||
writeToDisk: true,
|
||||
stats: {
|
||||
colors: true,
|
||||
chunks: false
|
||||
}
|
||||
}))
|
||||
app.use(
|
||||
webpackDevMiddleware(webpack(WebpackConfig), {
|
||||
publicPath: '/__build__/',
|
||||
writeToDisk: true,
|
||||
stats: {
|
||||
colors: true,
|
||||
chunks: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter(file => file !== 'ssr')
|
||||
.forEach((file) => {
|
||||
.forEach(file => {
|
||||
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
|
||||
app.use(rewrite(`/${file}/*`, `/${file}/index.html`))
|
||||
}
|
||||
|
||||
+38
-35
@@ -6,7 +6,7 @@ Vue.use(VueMeta, {
|
||||
tagIDKeyName: 'hid'
|
||||
}) */
|
||||
|
||||
export default function createMyApp () {
|
||||
export default function createMyApp() {
|
||||
const Home = {
|
||||
template: `<div>
|
||||
<router-link to="/about">About</router-link>
|
||||
@@ -19,15 +19,15 @@ export default function createMyApp () {
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Hello World'
|
||||
content: 'Hello World',
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'Hello World'
|
||||
}
|
||||
]
|
||||
}
|
||||
content: 'Hello World',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const About = {
|
||||
@@ -42,28 +42,28 @@ export default function createMyApp () {
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'About World'
|
||||
content: 'About World',
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'About World'
|
||||
}
|
||||
]
|
||||
}
|
||||
content: 'About World',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory('/ssr'),
|
||||
routes: [
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/about', component: About }
|
||||
]
|
||||
{ path: '/about', component: About },
|
||||
],
|
||||
})
|
||||
|
||||
const app = createSSRApp({
|
||||
router,
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: 'Boring Title',
|
||||
htmlAttrs: { amp: true },
|
||||
@@ -74,59 +74,63 @@ export default function createMyApp () {
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
template: chunk => `${chunk} - My Site`,
|
||||
content: 'Default Title'
|
||||
content: 'Default Title',
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'Say something'
|
||||
}
|
||||
content: 'Say something',
|
||||
},
|
||||
],
|
||||
script: [
|
||||
{
|
||||
hid: 'ldjson-schema',
|
||||
type: 'application/ld+json',
|
||||
innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }'
|
||||
}, {
|
||||
innerHTML:
|
||||
'{ "@context": "http://www.schema.org", "@type": "Organization" }',
|
||||
},
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: '{ "body": "yes" }',
|
||||
body: true
|
||||
}, {
|
||||
body: true,
|
||||
},
|
||||
{
|
||||
hid: 'my-async-script-with-load-callback',
|
||||
src: '/user-1.js',
|
||||
body: true,
|
||||
defer: true,
|
||||
callback: this.loadCallback
|
||||
}, {
|
||||
callback: this.loadCallback,
|
||||
},
|
||||
{
|
||||
skip: this.count < 1,
|
||||
src: '/user-2.js',
|
||||
body: true,
|
||||
callback: this.loadCallback
|
||||
}
|
||||
callback: this.loadCallback,
|
||||
},
|
||||
],
|
||||
__dangerouslyDisableSanitizersByTagID: {
|
||||
'ldjson-schema': ['innerHTML']
|
||||
}
|
||||
'ldjson-schema': ['innerHTML'],
|
||||
},
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
count: 0,
|
||||
users: process.server ? [] : window.users
|
||||
users: process.server ? [] : window.users,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
const { set, remove } = this.$meta().addApp('client-only')
|
||||
set({
|
||||
bodyAttrs: { class: 'client-only' }
|
||||
bodyAttrs: { class: 'client-only' },
|
||||
})
|
||||
|
||||
setTimeout(() => remove(), 3000)
|
||||
},
|
||||
methods: {
|
||||
loadCallback () {
|
||||
loadCallback() {
|
||||
this.count++
|
||||
}
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div id="app">
|
||||
@@ -142,8 +146,7 @@ export default function createMyApp () {
|
||||
</ul>
|
||||
|
||||
<router-view />
|
||||
</div>`
|
||||
|
||||
</div>`,
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
|
||||
@@ -12,7 +12,7 @@ const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
|
||||
|
||||
process.server = true
|
||||
|
||||
export async function renderPage ({ url }) {
|
||||
export async function renderPage({ url }) {
|
||||
const { app, router } = await createApp()
|
||||
|
||||
await router.push(url.substr(4))
|
||||
@@ -30,17 +30,17 @@ export async function renderPage ({ url }) {
|
||||
const pageHtml = compiled({
|
||||
app: appHtml,
|
||||
htmlAttrs: {
|
||||
text: () => {}
|
||||
text: () => {},
|
||||
},
|
||||
headAttrs: {
|
||||
text: () => {}
|
||||
text: () => {},
|
||||
},
|
||||
bodyAttrs: {
|
||||
text: () => {}
|
||||
text: () => {},
|
||||
},
|
||||
head: () => {},
|
||||
bodyPrepend: () => {},
|
||||
bodyAppend: () => {}
|
||||
bodyAppend: () => {},
|
||||
// ...app.$meta().inject()
|
||||
})
|
||||
|
||||
|
||||
+77
-58
@@ -1,7 +1,15 @@
|
||||
import { createApp, defineComponent, reactive, inject, toRefs, h, watch } from 'vue'
|
||||
import {
|
||||
createApp,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
reactive,
|
||||
inject,
|
||||
toRefs,
|
||||
h,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Metainfo from '../next/Metainfo.vue'
|
||||
import { createMeta, useMeta } from '../next'
|
||||
import { createManager, useMeta, useMetainfo } from '../../src'
|
||||
// import About from './about.vue'
|
||||
|
||||
const metaUpdated = 'no'
|
||||
@@ -9,47 +17,48 @@ const metaUpdated = 'no'
|
||||
const ChildComponent = defineComponent({
|
||||
name: 'child-component',
|
||||
props: {
|
||||
page: String
|
||||
page: String,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
|
||||
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
|
||||
</div>`,
|
||||
setup (props) {
|
||||
setup(props) {
|
||||
const state = reactive({
|
||||
date: null,
|
||||
metaUpdated
|
||||
metaUpdated,
|
||||
})
|
||||
|
||||
const title = props.page[0].toUpperCase() + props.page.slice(1)
|
||||
|
||||
console.log('ChildComponent Setup')
|
||||
useMeta({
|
||||
charset: 'utf16',
|
||||
title,
|
||||
description: 'Description ' + props.page,
|
||||
og: {
|
||||
title: 'Og Title ' + props.page
|
||||
}
|
||||
title: 'Og Title ' + props.page,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state)
|
||||
...toRefs(state),
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function view (page) {
|
||||
function view(page) {
|
||||
return {
|
||||
name: `section-${page}`,
|
||||
render () {
|
||||
render() {
|
||||
return h(ChildComponent, { page })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const App = {
|
||||
setup () {
|
||||
setup() {
|
||||
// console.log('App', getCurrentInstance())
|
||||
const { meta } = useMeta({
|
||||
base: { href: '/vue-router', target: '_blank' },
|
||||
charset: 'utf8',
|
||||
@@ -60,23 +69,23 @@ const App = {
|
||||
description: 'Bla bla',
|
||||
image: [
|
||||
'https://picsum.photos/600/400/?image=80',
|
||||
'https://picsum.photos/600/400/?image=82'
|
||||
]
|
||||
'https://picsum.photos/600/400/?image=82',
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title: 'Twitter Title'
|
||||
title: 'Twitter Title',
|
||||
},
|
||||
noscript: [
|
||||
'<!-- // A code comment -->',
|
||||
{ tag: 'link', rel: 'stylesheet', href: 'style.css' }
|
||||
{ tag: 'link', rel: 'stylesheet', href: 'style.css' },
|
||||
],
|
||||
otherNoscript: {
|
||||
tag: 'noscript',
|
||||
'data-test': 'hello',
|
||||
content: [
|
||||
'<!-- // Another code comment -->',
|
||||
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' }
|
||||
]
|
||||
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' },
|
||||
],
|
||||
},
|
||||
body: 'body-script1.js',
|
||||
script: [
|
||||
@@ -84,26 +93,32 @@ const App = {
|
||||
{ src: 'head-script1.js' },
|
||||
'<![endif]-->',
|
||||
{ src: 'body-script2.js', target: 'body' },
|
||||
{ src: 'body-script3.js', target: '#put-it-here' }
|
||||
{ src: 'body-script3.js', target: '#put-it-here' },
|
||||
],
|
||||
esi: {
|
||||
content: [{
|
||||
tag: 'choose',
|
||||
content: [{
|
||||
tag: 'when',
|
||||
test: '$(HTTP_COOKIE{group})=="Advanced"',
|
||||
content: [{
|
||||
tag: 'include',
|
||||
src: 'http://www.example.com/advanced.html'
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
content: [
|
||||
{
|
||||
tag: 'choose',
|
||||
content: [
|
||||
{
|
||||
tag: 'when',
|
||||
test: '$(HTTP_COOKIE{group})=="Advanced"',
|
||||
content: [
|
||||
{
|
||||
tag: 'include',
|
||||
src: 'http://www.example.com/advanced.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
setTimeout(() => (meta.title = 'My Updated Title'), 2000)
|
||||
|
||||
const metainfo = inject('metainfo')
|
||||
const metainfo = useMetainfo()
|
||||
|
||||
window.$metainfo = metainfo
|
||||
|
||||
@@ -112,7 +127,7 @@ const App = {
|
||||
})
|
||||
|
||||
return {
|
||||
metainfo
|
||||
metainfo,
|
||||
}
|
||||
},
|
||||
template: `
|
||||
@@ -133,56 +148,60 @@ const App = {
|
||||
</transition>
|
||||
<p>Inspect Element to see the meta info</p>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
}
|
||||
|
||||
function decisionMaker5000000 (key, options, currentValue) {
|
||||
function decisionMaker5000000(key, pathSegments, getOptions, getCurrentValue) {
|
||||
let theChosenOne
|
||||
|
||||
const options = getOptions()
|
||||
|
||||
for (const option of options) {
|
||||
if (!theChosenOne || theChosenOne.context.uid < option.context.uid) {
|
||||
if (!theChosenOne || theChosenOne.context.vm.uid < option.context.vm.uid) {
|
||||
theChosenOne = option
|
||||
}
|
||||
}
|
||||
|
||||
console.log(key, currentValue, options.map(({ value }) => value))
|
||||
console.log(
|
||||
key,
|
||||
getCurrentValue(),
|
||||
options.map(({ value }) => value)
|
||||
)
|
||||
console.log(theChosenOne.value)
|
||||
return theChosenOne.value
|
||||
}
|
||||
|
||||
const meta = createMeta({
|
||||
const metaManager = createManager({
|
||||
resolver: decisionMaker5000000,
|
||||
config: {
|
||||
esi: {
|
||||
group: true,
|
||||
namespaced: true,
|
||||
contentAttributes: [
|
||||
'src',
|
||||
'test',
|
||||
'text'
|
||||
]
|
||||
}
|
||||
}
|
||||
contentAttributes: ['src', 'test', 'text'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useMeta(
|
||||
{
|
||||
og: {
|
||||
something: 'test',
|
||||
},
|
||||
},
|
||||
metaManager
|
||||
)
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/vue-router'),
|
||||
routes: [
|
||||
{ name: 'home', path: '/', component: view('home') },
|
||||
{ name: 'about', path: '/about', component: view('about') }
|
||||
]
|
||||
})
|
||||
|
||||
useMeta({
|
||||
og: {
|
||||
something: 'test'
|
||||
}
|
||||
{ name: 'about', path: '/about', component: view('about') },
|
||||
],
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.component('metainfo', Metainfo)
|
||||
app.use(router)
|
||||
app.use(meta)
|
||||
app.use(metaManager)
|
||||
app.mount('#app')
|
||||
|
||||
// old stuff:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/global.css">
|
||||
<link rel="stylesheet" href="/_static/global.css">
|
||||
<style>
|
||||
.page-enter-active, .page-leave-active {
|
||||
transition: opacity .5s
|
||||
|
||||
@@ -12,6 +12,6 @@ export default new Router({
|
||||
base: '/vuex-async',
|
||||
routes: [
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/posts/:slug', component: Post }
|
||||
]
|
||||
{ path: '/posts/:slug', component: Post },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -14,60 +14,64 @@ export default new Vuex.Store({
|
||||
title: '',
|
||||
content: '',
|
||||
slug: '',
|
||||
published: false
|
||||
published: false,
|
||||
},
|
||||
posts: [{
|
||||
slug: 'a-sample-blog-post',
|
||||
title: 'A Sample Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: true
|
||||
}, {
|
||||
slug: 'an-unpublished-blog-post',
|
||||
title: 'An Unpublished Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: false
|
||||
}, {
|
||||
slug: 'another-blog-post',
|
||||
title: 'Another Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: true
|
||||
}]
|
||||
posts: [
|
||||
{
|
||||
slug: 'a-sample-blog-post',
|
||||
title: 'A Sample Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: true,
|
||||
},
|
||||
{
|
||||
slug: 'an-unpublished-blog-post',
|
||||
title: 'An Unpublished Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: false,
|
||||
},
|
||||
{
|
||||
slug: 'another-blog-post',
|
||||
title: 'Another Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// GETTERS
|
||||
getters: {
|
||||
isLoading (state) {
|
||||
isLoading(state) {
|
||||
return state.isLoading
|
||||
},
|
||||
post (state) {
|
||||
post(state) {
|
||||
return state.post
|
||||
},
|
||||
publishedPosts (state) {
|
||||
publishedPosts(state) {
|
||||
return state.posts.filter(post => post.published)
|
||||
},
|
||||
publishedPostsCount (state, getters) {
|
||||
publishedPostsCount(state, getters) {
|
||||
return getters.publishedPosts.length
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// MUTATIONS
|
||||
mutations: {
|
||||
loadingState (state, { isLoading }) {
|
||||
loadingState(state, { isLoading }) {
|
||||
state.isLoading = isLoading
|
||||
},
|
||||
getPost (state, { slug }) {
|
||||
getPost(state, { slug }) {
|
||||
state.post = state.posts.find(post => post.slug === slug)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// ACTIONS
|
||||
actions: {
|
||||
getPost ({ commit }, payload) {
|
||||
getPost({ commit }, payload) {
|
||||
commit('loadingState', { isLoading: true })
|
||||
setTimeout(() => {
|
||||
commit('getPost', payload)
|
||||
commit('loadingState', { isLoading: false })
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -13,6 +13,6 @@ export default new Router({
|
||||
base: '/vuex',
|
||||
routes: [
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/posts/:slug', component: Post }
|
||||
]
|
||||
{ path: '/posts/:slug', component: Post },
|
||||
],
|
||||
})
|
||||
|
||||
+30
-26
@@ -14,50 +14,54 @@ export default new Vuex.Store({
|
||||
title: '',
|
||||
content: '',
|
||||
slug: '',
|
||||
published: false
|
||||
published: false,
|
||||
},
|
||||
posts: [{
|
||||
slug: 'a-sample-blog-post',
|
||||
title: 'A Sample Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: true
|
||||
}, {
|
||||
slug: 'an-unpublished-blog-post',
|
||||
title: 'An Unpublished Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: false
|
||||
}, {
|
||||
slug: 'another-blog-post',
|
||||
title: 'Another Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: true
|
||||
}]
|
||||
posts: [
|
||||
{
|
||||
slug: 'a-sample-blog-post',
|
||||
title: 'A Sample Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: true,
|
||||
},
|
||||
{
|
||||
slug: 'an-unpublished-blog-post',
|
||||
title: 'An Unpublished Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: false,
|
||||
},
|
||||
{
|
||||
slug: 'another-blog-post',
|
||||
title: 'Another Blog Post',
|
||||
content: 'This is the blog post content',
|
||||
published: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// GETTERS
|
||||
getters: {
|
||||
post (state) {
|
||||
post(state) {
|
||||
return state.post
|
||||
},
|
||||
publishedPosts (state) {
|
||||
publishedPosts(state) {
|
||||
return state.posts.filter(post => post.published)
|
||||
},
|
||||
publishedPostsCount (state, getters) {
|
||||
publishedPostsCount(state, getters) {
|
||||
return getters.publishedPosts.length
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// MUTATIONS
|
||||
mutations: {
|
||||
getPost (state, { slug }) {
|
||||
getPost(state, { slug }) {
|
||||
state.post = state.posts.find(post => post.slug === slug)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// ACTIONS
|
||||
actions: {
|
||||
getPost ({ commit }, payload) {
|
||||
getPost({ commit }, payload) {
|
||||
commit('getPost', payload)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import webpack from 'webpack'
|
||||
import WebpackBar from 'webpackbar'
|
||||
import { VueLoaderPlugin } from 'vue-loader'
|
||||
|
||||
// const srcDir = path.join(__dirname, '..', 'src')
|
||||
|
||||
export default {
|
||||
devtool: 'inline-source-map',
|
||||
mode: 'development',
|
||||
entry: fs.readdirSync(__dirname)
|
||||
.reduce((entries, dir) => {
|
||||
const fullDir = path.join(__dirname, dir)
|
||||
|
||||
if (dir === 'ssr') {
|
||||
entries[dir] = path.join(fullDir, 'browser.js')
|
||||
} else if (dir === 'vue-router') {
|
||||
const entry = path.join(fullDir, 'app.js')
|
||||
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
|
||||
entries[dir] = entry
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}, {}),
|
||||
output: {
|
||||
path: path.join(__dirname, '__build__'),
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[id].chunk.js',
|
||||
publicPath: '/__build__/'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
useBuiltIns: 'usage',
|
||||
corejs: '3',
|
||||
targets: { ie: 9, safari: '5.1' }
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
resourceQuery: /blockType=head/,
|
||||
loader: require.resolve('./meta-loader.js')
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: 'vue-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// this isn't technically needed, since the default `vue` entry for bundlers
|
||||
// is a simple `export * from '@vue/runtime-dom`. However having this
|
||||
// extra re-export somehow causes webpack to always invalidate the module
|
||||
// on the first HMR update and causes the page to reload.
|
||||
vue: 'vue/dist/vue.esm-bundler.js',
|
||||
'vue-meta': path.resolve(__dirname, './next/')
|
||||
}
|
||||
},
|
||||
// Expose __dirname to allow automatically setting basename.
|
||||
context: __dirname,
|
||||
node: {
|
||||
__dirname: true
|
||||
},
|
||||
plugins: [
|
||||
new WebpackBar(),
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||
})
|
||||
],
|
||||
devServer: {
|
||||
inline: true,
|
||||
hot: true,
|
||||
stats: 'minimal',
|
||||
contentBase: __dirname,
|
||||
overlay: true
|
||||
}
|
||||
}
|
||||
+54
-85
@@ -1,55 +1,41 @@
|
||||
{
|
||||
"name": "vue-meta",
|
||||
"version": "2.3.3",
|
||||
"description": "Manage HTML metadata in Vue.js components with ssr support",
|
||||
"keywords": [
|
||||
"attribute",
|
||||
"google",
|
||||
"head",
|
||||
"helmet",
|
||||
"info",
|
||||
"metadata",
|
||||
"meta",
|
||||
"seo",
|
||||
"server",
|
||||
"ssr",
|
||||
"title",
|
||||
"universal",
|
||||
"vue"
|
||||
"description": "Manage HTML metadata in Vue.js components with SSR support",
|
||||
"main": "dist/vue-meta.common.js",
|
||||
"web": "dist/vue-meta.js",
|
||||
"module": "dist/vue-meta.esm.js",
|
||||
"typings": "types/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"types/*.d.ts"
|
||||
],
|
||||
"homepage": "https://github.com/nuxt/vue-meta",
|
||||
"bugs": "https://github.com/nuxt/vue-meta/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com/nuxt/vue-meta.git"
|
||||
"url": "git+https://github.com/nuxt/vue-meta.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Declan de Wet (@declandewet)"
|
||||
},
|
||||
{
|
||||
"name": "Sebastien Chopin (@Atinux)"
|
||||
},
|
||||
{
|
||||
"name": "Pim (@pimlie)"
|
||||
}
|
||||
"keywords": [
|
||||
"google",
|
||||
"head",
|
||||
"metadata",
|
||||
"meta",
|
||||
"seo",
|
||||
"ssr",
|
||||
"title",
|
||||
"universal",
|
||||
"vue"
|
||||
],
|
||||
"files": [
|
||||
"dist",
|
||||
"types/*.d.ts"
|
||||
],
|
||||
"main": "dist/vue-meta.common.js",
|
||||
"web": "dist/vue-meta.js",
|
||||
"module": "dist/vue-meta.esm.js",
|
||||
"typings": "types/index.d.ts",
|
||||
"author": "Pim (@pimlie)",
|
||||
"scripts": {
|
||||
"build": "rimraf dist && rollup -c scripts/rollup.config.js",
|
||||
"coverage": "codecov",
|
||||
"dev": "cd examples && yarn dev && cd ..",
|
||||
"dev": "webpack-dev-server --mode=development",
|
||||
"docs": "vuepress dev --host 0.0.0.0 --port 3000 docs",
|
||||
"docs:build": "vuepress build docs",
|
||||
"lint": "eslint src test examples",
|
||||
"lint": "prettier -c --parser typescript \"{examples,src,test,e2e}/**/*.[jt]s?(x)\"",
|
||||
"prerelease": "git checkout master && git pull -r",
|
||||
"release": "yarn lint && yarn test && standard-version",
|
||||
"test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser",
|
||||
@@ -58,69 +44,52 @@
|
||||
"test:unit": "jest test/unit",
|
||||
"test:types": "tsc -p types/test"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2"
|
||||
"pperDependencies": {
|
||||
"vue": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/node": "^7.8.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"@nuxtjs/eslint-config": "^2.0.2",
|
||||
"@vue/server-test-utils": "^1.0.0-beta.30",
|
||||
"@vue/test-utils": "^1.0.0-beta.30",
|
||||
"@vuepress/plugin-google-analytics": "^1.4.0",
|
||||
"@vuepress/plugin-pwa": "^1.4.0",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^25.2.3",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.0",
|
||||
"@types/webpack": "^4.41.16",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@vue/compiler-sfc": "^3.0.0-beta.14",
|
||||
"@vue/server-test-utils": "^1.0.3",
|
||||
"@vue/test-utils": "^1.0.3",
|
||||
"@wishy-gift/html-include-chunks-webpack-plugin": "^0.1.5",
|
||||
"browserstack-local": "^1.4.5",
|
||||
"chromedriver": "^80.0.1",
|
||||
"codecov": "^3.6.5",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-jest": "^23.8.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"esm": "^3.2.25",
|
||||
"fs-extra": "^9.0.0",
|
||||
"chromedriver": "^83.0.0",
|
||||
"codecov": "^3.7.0",
|
||||
"geckodriver": "^1.19.1",
|
||||
"get-port": "^5.1.1",
|
||||
"hable": "3.0.0",
|
||||
"is-wsl": "^2.1.1",
|
||||
"jest": "^25.2.3",
|
||||
"jest-environment-jsdom": "^25.2.3",
|
||||
"jest-environment-jsdom-global": "^1.2.1",
|
||||
"jsdom": "^16.2.1",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"jest": "^26.0.1",
|
||||
"jest-environment-jsdom": "^26.0.1",
|
||||
"jest-environment-jsdom-global": "^2.0.2",
|
||||
"jsdom": "^16.2.2",
|
||||
"lodash": "^4.17.15",
|
||||
"node-env-file": "^0.1.8",
|
||||
"puppeteer-core": "^2.1.1",
|
||||
"prettier": "^2.0.5",
|
||||
"puppeteer-core": "^3.2.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.2.0",
|
||||
"rollup": "^2.11.2",
|
||||
"rollup-plugin-babel": "^4.4.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"rollup-plugin-terser": "^6.1.0",
|
||||
"rollup-plugin-typescript2": "^0.27.1",
|
||||
"selenium-webdriver": "^4.0.0-alpha.5",
|
||||
"standard-version": "^7.1.0",
|
||||
"standard-version": "^8.0.0",
|
||||
"tib": "^0.7.4",
|
||||
"typescript": "^3.8.3",
|
||||
"vue": "^3.0.0-alpha.10",
|
||||
"ts-jest": "^26.1.0",
|
||||
"ts-loader": "^7.0.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "^3.9.3",
|
||||
"vue": "next",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-loader": "^15.9.1",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuepress": "^1.4.0",
|
||||
"vuepress-theme-vue": "^1.1.0",
|
||||
"webpack": "^4.42.1"
|
||||
"vue-loader": "^16.0.0-beta.2",
|
||||
"vue-router": "next",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-bundle-analyzer": "^3.8.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { h, defineComponent, Teleport, PropType, VNode, VNodeProps } from 'vue'
|
||||
import { isArray } from '@vue/shared'
|
||||
import { renderMeta } from './render'
|
||||
import { getCurrentManager } from './useApi'
|
||||
import { MetainfoActive } from './types'
|
||||
|
||||
export interface MetainfoProps {
|
||||
metainfo: MetainfoActive
|
||||
}
|
||||
|
||||
export function addVnode(targets: any, target: string, vnode: VNode) {
|
||||
if (!targets[target]) {
|
||||
targets[target] = []
|
||||
}
|
||||
|
||||
targets[target].push(vnode)
|
||||
}
|
||||
|
||||
export const MetainfoImpl = defineComponent({
|
||||
name: 'Metainfo',
|
||||
props: {
|
||||
metainfo: {
|
||||
type: Object as PropType<MetainfoActive>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup({ metainfo }, { slots }) {
|
||||
return () => {
|
||||
const targets: any = {}
|
||||
|
||||
const manager = getCurrentManager()
|
||||
|
||||
for (const key in metainfo) {
|
||||
const config = manager.config[key] || {}
|
||||
|
||||
const vnodes = renderMeta(
|
||||
{ metainfo, slots },
|
||||
key,
|
||||
metainfo[key],
|
||||
config
|
||||
)
|
||||
let defaultTarget =
|
||||
(key !== 'base' && metainfo[key].target) || config.target || 'head'
|
||||
|
||||
if (isArray(vnodes)) {
|
||||
for (const { target, vnode } of vnodes) {
|
||||
addVnode(targets, target || defaultTarget, vnode)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const { target, vnode } = vnodes
|
||||
addVnode(targets, target || defaultTarget, vnode)
|
||||
}
|
||||
|
||||
// console.log('TARGETS', targets)
|
||||
return Object.keys(targets).map(target => {
|
||||
return h(Teleport, { to: target }, targets[target])
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const Metainfo = (MetainfoImpl as any) as {
|
||||
new (): {
|
||||
$props: VNodeProps & MetainfoProps
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { toArray } from '../utils/array'
|
||||
import { querySelector, removeAttribute } from '../utils/elements'
|
||||
|
||||
const callbacks = []
|
||||
|
||||
export function isDOMLoaded (d) {
|
||||
return (d || document).readyState !== 'loading'
|
||||
}
|
||||
|
||||
export function isDOMComplete (d) {
|
||||
return (d || document).readyState === 'complete'
|
||||
}
|
||||
|
||||
export function waitDOMLoaded () {
|
||||
if (isDOMLoaded()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve))
|
||||
}
|
||||
|
||||
export function addCallback (query, callback) {
|
||||
if (arguments.length === 1) {
|
||||
callback = query
|
||||
query = ''
|
||||
}
|
||||
|
||||
callbacks.push([query, callback])
|
||||
}
|
||||
|
||||
export function addCallbacks ({ tagIDKeyName }, type, tags, autoAddListeners) {
|
||||
let hasAsyncCallback = false
|
||||
|
||||
tags.forEach((tag) => {
|
||||
if (!tag[tagIDKeyName] || !tag.callback) {
|
||||
return
|
||||
}
|
||||
|
||||
hasAsyncCallback = true
|
||||
addCallback(`${type}[data-${tagIDKeyName}="${tag[tagIDKeyName]}"]`, tag.callback)
|
||||
})
|
||||
|
||||
if (!autoAddListeners || !hasAsyncCallback) {
|
||||
return hasAsyncCallback
|
||||
}
|
||||
|
||||
return addListeners()
|
||||
}
|
||||
|
||||
export function addListeners () {
|
||||
if (isDOMComplete()) {
|
||||
applyCallbacks()
|
||||
return
|
||||
}
|
||||
|
||||
// Instead of using a MutationObserver, we just apply
|
||||
/* istanbul ignore next */
|
||||
document.onreadystatechange = () => {
|
||||
applyCallbacks()
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCallbacks (matchElement) {
|
||||
callbacks.forEach((args) => {
|
||||
// do not use destructuring for args, it increases transpiled size
|
||||
// due to var checks while we are guaranteed the structure of the cb
|
||||
const query = args[0]
|
||||
const callback = args[1]
|
||||
const selector = `${query}[onload="this.__vm_l=1"]`
|
||||
|
||||
let elements = []
|
||||
if (!matchElement) {
|
||||
elements = toArray(querySelector(selector))
|
||||
}
|
||||
|
||||
if (matchElement && matchElement.matches(selector)) {
|
||||
elements = [matchElement]
|
||||
}
|
||||
|
||||
elements.forEach((element) => {
|
||||
/* __vm_cb: whether the load callback has been called
|
||||
* __vm_l: set by onload attribute, whether the element was loaded
|
||||
* __vm_ev: whether the event listener was added or not
|
||||
*/
|
||||
if (element.__vm_cb) {
|
||||
return
|
||||
}
|
||||
|
||||
const onload = () => {
|
||||
/* Mark that the callback for this element has already been called,
|
||||
* this prevents the callback to run twice in some (rare) conditions
|
||||
*/
|
||||
element.__vm_cb = true
|
||||
|
||||
/* onload needs to be removed because we only need the
|
||||
* attribute after ssr and if we dont remove it the node
|
||||
* will fail isEqualNode on the client
|
||||
*/
|
||||
removeAttribute(element, 'onload')
|
||||
|
||||
callback(element)
|
||||
}
|
||||
|
||||
/* IE9 doesnt seem to load scripts synchronously,
|
||||
* causing a script sometimes/often already to be loaded
|
||||
* when we add the event listener below (thus adding an onload event
|
||||
* listener has no use because it will never be triggered).
|
||||
* Therefore we add the onload attribute during ssr, and
|
||||
* check here if it was already loaded or not
|
||||
*/
|
||||
if (element.__vm_l) {
|
||||
onload()
|
||||
return
|
||||
}
|
||||
|
||||
if (!element.__vm_ev) {
|
||||
element.__vm_ev = true
|
||||
|
||||
element.addEventListener('load', onload)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { clientSequences } from '../shared/escaping'
|
||||
import { rootConfigKey } from '../shared/constants'
|
||||
import { showWarningNotSupported } from '../shared/log'
|
||||
import { getComponentMetaInfo } from '../shared/getComponentOption'
|
||||
import { getAppsMetaInfo, clearAppsMetaInfo } from '../shared/additional-app'
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import { isFunction } from '../utils/is-type'
|
||||
import updateClientMetaInfo from './updateClientMetaInfo'
|
||||
|
||||
/**
|
||||
* When called, will update the current meta info with new meta info.
|
||||
* Useful when updating meta info as the result of an asynchronous
|
||||
* action that resolves after the initial render takes place.
|
||||
*
|
||||
* Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion
|
||||
* to implement this method.
|
||||
*
|
||||
* @return {Object} - new meta info
|
||||
*/
|
||||
export default function refresh (rootVm, options) {
|
||||
options = options || {}
|
||||
|
||||
// make sure vue-meta was initiated
|
||||
if (!rootVm[rootConfigKey]) {
|
||||
showWarningNotSupported()
|
||||
return {}
|
||||
}
|
||||
|
||||
// collect & aggregate all metaInfo $options
|
||||
const rawInfo = getComponentMetaInfo(options, rootVm)
|
||||
|
||||
const metaInfo = getMetaInfo(options, rawInfo, clientSequences, rootVm)
|
||||
|
||||
const { appId } = rootVm[rootConfigKey]
|
||||
let tags = updateClientMetaInfo(appId, options, metaInfo)
|
||||
|
||||
// emit "event" with new info
|
||||
if (tags && isFunction(metaInfo.changed)) {
|
||||
metaInfo.changed(metaInfo, tags.tagsAdded, tags.tagsRemoved)
|
||||
|
||||
tags = {
|
||||
addedTags: tags.tagsAdded,
|
||||
removedTags: tags.tagsRemoved
|
||||
}
|
||||
}
|
||||
|
||||
const appsMetaInfo = getAppsMetaInfo()
|
||||
if (appsMetaInfo) {
|
||||
for (const additionalAppId in appsMetaInfo) {
|
||||
updateClientMetaInfo(additionalAppId, options, appsMetaInfo[additionalAppId])
|
||||
delete appsMetaInfo[additionalAppId]
|
||||
}
|
||||
clearAppsMetaInfo(true)
|
||||
}
|
||||
|
||||
return {
|
||||
vm: rootVm,
|
||||
metaInfo: metaInfo, // eslint-disable-line object-shorthand
|
||||
tags
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { rootConfigKey } from '../shared/constants'
|
||||
|
||||
// store an id to keep track of DOM updates
|
||||
let batchId = null
|
||||
|
||||
export function triggerUpdate ({ debounceWait }, rootVm, hookName) {
|
||||
// if an update was triggered during initialization or when an update was triggered by the
|
||||
// metaInfo watcher, set initialized to null
|
||||
// then we keep falsy value but know we need to run a triggerUpdate after initialization
|
||||
if (!rootVm[rootConfigKey].initialized && (rootVm[rootConfigKey].initializing || hookName === 'watcher')) {
|
||||
rootVm[rootConfigKey].initialized = null
|
||||
}
|
||||
|
||||
if (rootVm[rootConfigKey].initialized && !rootVm[rootConfigKey].pausing) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
// eslint-disable-next-line no-void
|
||||
batchUpdate(() => void rootVm.$meta().refresh(), debounceWait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a batched update.
|
||||
*
|
||||
* @param {(null|Number)} id - the ID of this update
|
||||
* @param {Function} callback - the update to perform
|
||||
* @return {Number} id - a new ID
|
||||
*/
|
||||
export function batchUpdate (callback, timeout) {
|
||||
timeout = timeout === undefined ? 10 : timeout
|
||||
|
||||
if (!timeout) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(batchId)
|
||||
batchId = setTimeout(() => {
|
||||
callback()
|
||||
}, timeout)
|
||||
|
||||
return batchId
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { metaInfoOptionKeys, metaInfoAttributeKeys, tagsSupportingOnload } from '../shared/constants'
|
||||
import { isArray } from '../utils/is-type'
|
||||
import { includes } from '../utils/array'
|
||||
import { getTag, removeAttribute } from '../utils/elements'
|
||||
import { addCallbacks, addListeners } from './load'
|
||||
import { updateAttribute, updateTag, updateTitle } from './updaters'
|
||||
|
||||
/**
|
||||
* Performs client-side updates when new meta info is received
|
||||
*
|
||||
* @param {Object} newInfo - the meta info to update to
|
||||
*/
|
||||
export default function updateClientMetaInfo (appId, options, newInfo) {
|
||||
options = options || {}
|
||||
const { ssrAttribute, ssrAppId } = options
|
||||
|
||||
// only cache tags for current update
|
||||
const tags = {}
|
||||
|
||||
const htmlTag = getTag(tags, 'html')
|
||||
|
||||
// if this is a server render, then dont update
|
||||
if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) {
|
||||
// remove the server render attribute so we can update on (next) changes
|
||||
removeAttribute(htmlTag, ssrAttribute)
|
||||
|
||||
// add load callbacks if the
|
||||
let addLoadListeners = false
|
||||
tagsSupportingOnload.forEach((type) => {
|
||||
if (newInfo[type] && addCallbacks(options, type, newInfo[type])) {
|
||||
addLoadListeners = true
|
||||
}
|
||||
})
|
||||
|
||||
if (addLoadListeners) {
|
||||
addListeners()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// initialize tracked changes
|
||||
const tagsAdded = {}
|
||||
const tagsRemoved = {}
|
||||
|
||||
for (const type in newInfo) {
|
||||
// ignore these
|
||||
if (includes(metaInfoOptionKeys, type)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'title') {
|
||||
// update the title
|
||||
updateTitle(newInfo.title)
|
||||
continue
|
||||
}
|
||||
|
||||
if (includes(metaInfoAttributeKeys, type)) {
|
||||
const tagName = type.substr(0, 4)
|
||||
updateAttribute(appId, options, type, newInfo[type], getTag(tags, tagName))
|
||||
continue
|
||||
}
|
||||
|
||||
// tags should always be an array, ignore if it isnt
|
||||
if (!isArray(newInfo[type])) {
|
||||
continue
|
||||
}
|
||||
|
||||
const { oldTags, newTags } = updateTag(
|
||||
appId,
|
||||
options,
|
||||
type,
|
||||
newInfo[type],
|
||||
getTag(tags, 'head'),
|
||||
getTag(tags, 'body')
|
||||
)
|
||||
|
||||
if (newTags.length) {
|
||||
tagsAdded[type] = newTags
|
||||
tagsRemoved[type] = oldTags
|
||||
}
|
||||
}
|
||||
|
||||
return { tagsAdded, tagsRemoved }
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { booleanHtmlAttributes } from '../../shared/constants'
|
||||
import { includes } from '../../utils/array'
|
||||
import { removeAttribute } from '../../utils/elements'
|
||||
|
||||
// keep a local map of attribute values
|
||||
// instead of adding it to the html
|
||||
export const attributeMap = {}
|
||||
|
||||
/**
|
||||
* Updates the document's html tag attributes
|
||||
*
|
||||
* @param {Object} attrs - the new document html attributes
|
||||
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
|
||||
*/
|
||||
export default function updateAttribute (appId, options, type, attrs, tag) {
|
||||
const { attribute } = options || {}
|
||||
|
||||
const vueMetaAttrString = tag.getAttribute(attribute)
|
||||
if (vueMetaAttrString) {
|
||||
attributeMap[type] = JSON.parse(decodeURI(vueMetaAttrString))
|
||||
removeAttribute(tag, attribute)
|
||||
}
|
||||
|
||||
const data = attributeMap[type] || {}
|
||||
|
||||
const toUpdate = []
|
||||
|
||||
// remove attributes from the map
|
||||
// which have been removed for this appId
|
||||
for (const attr in data) {
|
||||
if (data[attr] && appId in data[attr]) {
|
||||
toUpdate.push(attr)
|
||||
|
||||
if (!attrs[attr]) {
|
||||
delete data[attr][appId]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr in attrs) {
|
||||
const attrData = data[attr]
|
||||
|
||||
if (!attrData || attrData[appId] !== attrs[attr]) {
|
||||
toUpdate.push(attr)
|
||||
|
||||
if (attrs[attr]) {
|
||||
data[attr] = data[attr] || {}
|
||||
data[attr][appId] = attrs[attr]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr of toUpdate) {
|
||||
const attrData = data[attr]
|
||||
|
||||
const attrValues = []
|
||||
for (const appId in attrData) {
|
||||
Array.prototype.push.apply(attrValues, [].concat(attrData[appId]))
|
||||
}
|
||||
|
||||
if (attrValues.length) {
|
||||
const attrValue = includes(booleanHtmlAttributes, attr) && attrValues.some(Boolean)
|
||||
? ''
|
||||
: attrValues.filter(Boolean).join(' ')
|
||||
|
||||
tag.setAttribute(attr, attrValue)
|
||||
} else {
|
||||
removeAttribute(tag, attr)
|
||||
}
|
||||
}
|
||||
|
||||
attributeMap[type] = data
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as updateAttribute } from './attribute'
|
||||
export { default as updateTitle } from './title'
|
||||
export { default as updateTag } from './tag'
|
||||
@@ -1,141 +0,0 @@
|
||||
import { booleanHtmlAttributes, commonDataAttributes, tagProperties } from '../../shared/constants'
|
||||
import { includes } from '../../utils/array'
|
||||
import { queryElements, getElementsKey } from '../../utils/elements.js'
|
||||
|
||||
/**
|
||||
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
|
||||
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - a representation of what tags changed
|
||||
*/
|
||||
export default function updateTag (appId, options, type, tags, head, body) {
|
||||
const { attribute, tagIDKeyName } = options || {}
|
||||
|
||||
const dataAttributes = commonDataAttributes.slice()
|
||||
dataAttributes.push(tagIDKeyName)
|
||||
|
||||
const newElements = []
|
||||
|
||||
const queryOptions = { appId, attribute, type, tagIDKeyName }
|
||||
const currentElements = {
|
||||
head: queryElements(head, queryOptions),
|
||||
pbody: queryElements(body, queryOptions, { pbody: true }),
|
||||
body: queryElements(body, queryOptions, { body: true })
|
||||
}
|
||||
|
||||
if (tags.length > 1) {
|
||||
// remove duplicates that could have been found by merging tags
|
||||
// which include a mixin with metaInfo and that mixin is used
|
||||
// by multiple components on the same page
|
||||
const found = []
|
||||
tags = tags.filter((x) => {
|
||||
const k = JSON.stringify(x)
|
||||
const res = !includes(found, k)
|
||||
found.push(k)
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
tags.forEach((tag) => {
|
||||
if (tag.skip) {
|
||||
return
|
||||
}
|
||||
|
||||
const newElement = document.createElement(type)
|
||||
|
||||
if (!tag.once) {
|
||||
newElement.setAttribute(attribute, appId)
|
||||
}
|
||||
|
||||
Object.keys(tag).forEach((attr) => {
|
||||
/* istanbul ignore next */
|
||||
if (includes(tagProperties, attr)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (attr === 'innerHTML') {
|
||||
newElement.innerHTML = tag.innerHTML
|
||||
return
|
||||
}
|
||||
|
||||
if (attr === 'json') {
|
||||
newElement.innerHTML = JSON.stringify(tag.json)
|
||||
return
|
||||
}
|
||||
|
||||
if (attr === 'cssText') {
|
||||
if (newElement.styleSheet) {
|
||||
/* istanbul ignore next */
|
||||
newElement.styleSheet.cssText = tag.cssText
|
||||
} else {
|
||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (attr === 'callback') {
|
||||
newElement.onload = () => tag[attr](newElement)
|
||||
return
|
||||
}
|
||||
|
||||
const _attr = includes(dataAttributes, attr)
|
||||
? `data-${attr}`
|
||||
: attr
|
||||
|
||||
const isBooleanAttribute = includes(booleanHtmlAttributes, attr)
|
||||
if (isBooleanAttribute && !tag[attr]) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = isBooleanAttribute ? '' : tag[attr]
|
||||
newElement.setAttribute(_attr, value)
|
||||
})
|
||||
|
||||
const oldElements = currentElements[getElementsKey(tag)]
|
||||
|
||||
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
|
||||
let indexToDelete
|
||||
const hasEqualElement = oldElements.some((existingTag, index) => {
|
||||
indexToDelete = index
|
||||
return newElement.isEqualNode(existingTag)
|
||||
})
|
||||
|
||||
if (hasEqualElement && (indexToDelete || indexToDelete === 0)) {
|
||||
oldElements.splice(indexToDelete, 1)
|
||||
} else {
|
||||
newElements.push(newElement)
|
||||
}
|
||||
})
|
||||
|
||||
const oldElements = []
|
||||
for (const type in currentElements) {
|
||||
Array.prototype.push.apply(oldElements, currentElements[type])
|
||||
}
|
||||
|
||||
// remove old elements
|
||||
oldElements.forEach((element) => {
|
||||
element.parentNode.removeChild(element)
|
||||
})
|
||||
|
||||
// insert new elements
|
||||
newElements.forEach((element) => {
|
||||
if (element.hasAttribute('data-body')) {
|
||||
body.appendChild(element)
|
||||
return
|
||||
}
|
||||
|
||||
if (element.hasAttribute('data-pbody')) {
|
||||
body.insertBefore(element, body.firstChild)
|
||||
return
|
||||
}
|
||||
|
||||
head.appendChild(element)
|
||||
})
|
||||
|
||||
return {
|
||||
oldTags: oldElements,
|
||||
newTags: newElements
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Updates the document title
|
||||
*
|
||||
* @param {String} title - the new title of the document
|
||||
*/
|
||||
export default function updateTitle (title) {
|
||||
if (!title && title !== '') {
|
||||
return
|
||||
}
|
||||
|
||||
document.title = title
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { isArray } from '@vue/shared'
|
||||
import { tags } from './config/tags'
|
||||
import { TODO } from './types'
|
||||
|
||||
export interface ConfigOption {
|
||||
tag?: string
|
||||
target?: string
|
||||
group?: boolean
|
||||
nameAttribute?: string
|
||||
contentAttribute?: string
|
||||
nameless?: boolean
|
||||
namespaced?: boolean
|
||||
namespacedAttribute?: boolean
|
||||
}
|
||||
|
||||
const defaultMapping: { [key: string]: ConfigOption } = {
|
||||
body: {
|
||||
tag: 'script',
|
||||
target: 'body',
|
||||
},
|
||||
base: {
|
||||
contentAttribute: 'href',
|
||||
},
|
||||
charset: {
|
||||
tag: 'meta',
|
||||
nameless: true,
|
||||
contentAttribute: 'charset',
|
||||
},
|
||||
description: {
|
||||
tag: 'meta',
|
||||
},
|
||||
og: {
|
||||
group: true,
|
||||
namespacedAttribute: true,
|
||||
tag: 'meta',
|
||||
nameAttribute: 'property',
|
||||
},
|
||||
twitter: {
|
||||
group: true,
|
||||
namespacedAttribute: true,
|
||||
tag: 'meta',
|
||||
},
|
||||
}
|
||||
|
||||
export { defaultMapping }
|
||||
|
||||
export function hasConfig(name: string): boolean {
|
||||
return !!tags[name] || !!defaultMapping[name]
|
||||
}
|
||||
|
||||
export function getConfigKey(
|
||||
name: string | Array<string>,
|
||||
key: string,
|
||||
config: TODO,
|
||||
dontLog?: boolean
|
||||
): any {
|
||||
if (!dontLog) {
|
||||
// console.log('getConfigKey', name, key, getConfigKey(name, key, config, true), config)
|
||||
}
|
||||
|
||||
if (config && key in config) {
|
||||
return config[key]
|
||||
}
|
||||
|
||||
if (isArray(name)) {
|
||||
for (const _name of name) {
|
||||
if (_name && _name in tags) {
|
||||
return tags[_name][key]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (name in tags) {
|
||||
const tag = tags[name]
|
||||
return tag[key]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
export interface TagConfig {
|
||||
nameAttribute?: string
|
||||
contentAttributes: boolean | Array<string>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const tags: { [key: string]: TagConfig } = {
|
||||
title: {
|
||||
contentAttributes: false,
|
||||
},
|
||||
base: {
|
||||
contentAttributes: ['href', 'target'],
|
||||
},
|
||||
meta: {
|
||||
nameAttribute: 'name',
|
||||
contentAttributes: ['content', 'name', 'http-equiv', 'charset'],
|
||||
},
|
||||
link: {
|
||||
contentAttributes: [
|
||||
'href',
|
||||
'crossorigin',
|
||||
'rel',
|
||||
'media',
|
||||
'integrity',
|
||||
'hreflang',
|
||||
'type',
|
||||
'referrerpolicy',
|
||||
'sizes',
|
||||
'imagesrcset',
|
||||
'imagesizes',
|
||||
'as',
|
||||
'color',
|
||||
],
|
||||
},
|
||||
style: {
|
||||
contentAttributes: ['media'],
|
||||
},
|
||||
script: {
|
||||
contentAttributes: [
|
||||
'src',
|
||||
'type',
|
||||
'nomodule',
|
||||
'async',
|
||||
'defer',
|
||||
'crossorigin',
|
||||
'integrity',
|
||||
'referrerpolicy',
|
||||
],
|
||||
},
|
||||
noscript: {
|
||||
contentAttributes: false,
|
||||
},
|
||||
}
|
||||
|
||||
export { tags }
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// Global compile-time constants
|
||||
declare var __DEV__: boolean
|
||||
@@ -1,33 +0,0 @@
|
||||
import { version } from '../package.json'
|
||||
import { showWarningNotSupportedInBrowserBundle } from './shared/log'
|
||||
import createMixin from './shared/mixin'
|
||||
import { setOptions } from './shared/options'
|
||||
import $meta from './shared/$meta'
|
||||
import generate from './server/generate'
|
||||
import { hasMetaInfo } from './shared/meta-helpers'
|
||||
|
||||
/**
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
function install (Vue, options) {
|
||||
if (Vue.__vuemeta_installed) {
|
||||
return
|
||||
}
|
||||
Vue.__vuemeta_installed = true
|
||||
|
||||
options = setOptions(options)
|
||||
|
||||
Vue.prototype.$meta = function () {
|
||||
return $meta.call(this, options)
|
||||
}
|
||||
|
||||
Vue.mixin(createMixin(Vue, options))
|
||||
}
|
||||
|
||||
export default {
|
||||
version,
|
||||
install,
|
||||
generate: (metaInfo, options) => process.server ? generate(metaInfo, options) : showWarningNotSupportedInBrowserBundle('generate'),
|
||||
hasMetaInfo
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { createManager } from './manager'
|
||||
export * from './useApi'
|
||||
@@ -0,0 +1,4 @@
|
||||
import { markRaw, reactive } from 'vue'
|
||||
|
||||
export const shadow = markRaw({})
|
||||
export const active = reactive({})
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './globals'
|
||||
export * from './remove'
|
||||
export * from './set'
|
||||
export * from './update'
|
||||
@@ -0,0 +1,6 @@
|
||||
import { setByObject } from './set'
|
||||
import { MetaContext } from '../types'
|
||||
|
||||
export function remove(context: MetaContext) {
|
||||
setByObject(context, {})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { hasOwn } from '@vue/shared'
|
||||
import { clone } from '../utils'
|
||||
import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types'
|
||||
|
||||
export function resolveActive(
|
||||
context: MetaContext,
|
||||
key: string,
|
||||
pathSegments: PathSegments,
|
||||
shadowParent: ShadowNode,
|
||||
activeParent: ActiveNode
|
||||
) {
|
||||
let value
|
||||
|
||||
if (shadowParent[key].length > 1) {
|
||||
// Is this useful? Idea is to prevent the user from messing with these options by mistake
|
||||
const getShadow = () => Object.freeze(clone(shadowParent[key]))
|
||||
const getActive = () => Object.freeze(clone(activeParent[key]))
|
||||
|
||||
value = context.manager.resolver.resolve(
|
||||
key,
|
||||
pathSegments,
|
||||
getShadow,
|
||||
getActive
|
||||
)
|
||||
} else if (shadowParent[key].length) {
|
||||
value = shadowParent[key][0].value
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
delete activeParent[key]
|
||||
} else if (!hasOwn(activeParent, key) || activeParent[key] !== value) {
|
||||
activeParent[key] = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { isPlainObject, hasOwn } from '@vue/shared'
|
||||
import { shadow, active } from './globals'
|
||||
import { resolveActive } from './resolve'
|
||||
import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types'
|
||||
|
||||
export function set(
|
||||
context: MetaContext,
|
||||
key: string,
|
||||
value: any,
|
||||
shadowParent: ShadowNode = shadow,
|
||||
activeParent: ActiveNode = active,
|
||||
pathSegments: PathSegments = []
|
||||
) {
|
||||
if (isPlainObject(value)) {
|
||||
// shadow & active should always be in sync
|
||||
// if not we have bigger fish to fry
|
||||
if (!shadowParent[key]) {
|
||||
shadowParent[key] = {}
|
||||
activeParent[key] = {}
|
||||
}
|
||||
|
||||
return setByObject(
|
||||
context,
|
||||
value,
|
||||
shadowParent[key],
|
||||
activeParent[key],
|
||||
pathSegments
|
||||
)
|
||||
}
|
||||
|
||||
let idx = -1
|
||||
if (!shadowParent[key]) {
|
||||
shadowParent[key] = []
|
||||
} else {
|
||||
// check if we already have a value listed for this element for this context
|
||||
idx = shadowParent[key].findIndex(
|
||||
({ context: $context }: { context: MetaContext }) => $context === context
|
||||
)
|
||||
}
|
||||
|
||||
// if this context/key combo exists but value is undefined, remove it
|
||||
if (idx > -1 && value === undefined) {
|
||||
shadowParent[key].splice(idx, 1)
|
||||
|
||||
// overwrite current value for context/key combo
|
||||
} else if (idx > -1) {
|
||||
shadowParent[key][idx].value = value
|
||||
|
||||
// new context/key combo so just add value
|
||||
} else if (value) {
|
||||
shadowParent[key].push({ context, value })
|
||||
}
|
||||
|
||||
resolveActive(context, key, pathSegments, shadowParent, activeParent)
|
||||
}
|
||||
|
||||
export function setByObject(
|
||||
context: MetaContext,
|
||||
value: any,
|
||||
shadowParent: ShadowNode = shadow,
|
||||
activeParent: ActiveNode = active,
|
||||
pathSegments: PathSegments = []
|
||||
) {
|
||||
// cleanup properties that no longer exists
|
||||
for (const key in shadowParent) {
|
||||
if (hasOwn(value, key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (isPlainObject(shadowParent[key])) {
|
||||
setByObject(context, {}, shadowParent[key], activeParent[key], [
|
||||
...pathSegments,
|
||||
key,
|
||||
])
|
||||
continue
|
||||
}
|
||||
|
||||
set(context, key, undefined, shadowParent, activeParent, [
|
||||
...pathSegments,
|
||||
key,
|
||||
])
|
||||
}
|
||||
|
||||
// set new values
|
||||
for (const key in value) {
|
||||
set(context, key, value[key], shadowParent, activeParent, [
|
||||
...pathSegments,
|
||||
key,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { shadow, active } from './globals'
|
||||
import { set } from './set'
|
||||
import { MetaContext, PathSegments, ShadowNode, ActiveNode } from '../types'
|
||||
|
||||
export function update(
|
||||
context: MetaContext,
|
||||
pathSegments: PathSegments,
|
||||
key: string,
|
||||
value: any
|
||||
) {
|
||||
let shadowParent: ShadowNode = shadow
|
||||
let activeParent: ActiveNode = active
|
||||
|
||||
for (const segment of pathSegments) {
|
||||
shadowParent = shadowParent[segment]
|
||||
activeParent = activeParent[segment]
|
||||
}
|
||||
|
||||
set(context, key, value, shadowParent, activeParent)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { App } from 'vue'
|
||||
import { Metainfo } from './Metainfo'
|
||||
import { metaInfoKey } from './symbols'
|
||||
import { active } from './info/globals'
|
||||
import { Manager } from './manager'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentInternalInstance {
|
||||
$metaManager: Manager
|
||||
}
|
||||
}
|
||||
|
||||
export function applyMetaPlugin(app: App, manager: Manager) {
|
||||
app.component('Metainfo', Metainfo)
|
||||
|
||||
app.config.globalProperties.$metaManager = manager
|
||||
app.provide(metaInfoKey, active)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { App } from 'vue'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { defaultMapping } from './config'
|
||||
import { applyMetaPlugin } from './install'
|
||||
import * as deepestResolver from './resolvers/deepest'
|
||||
import { TODO, ActiveResolverObject, MetaContext, PathSegments } from './types'
|
||||
|
||||
export type ManagerOptions = {
|
||||
install(app: App): void
|
||||
}
|
||||
|
||||
export type Manager = {
|
||||
readonly config: TODO
|
||||
|
||||
resolver: ActiveResolverObject
|
||||
install(app: App): void
|
||||
}
|
||||
|
||||
export function createManager(options: TODO = {}): Manager {
|
||||
const { resolver = deepestResolver } = options
|
||||
|
||||
// TODO: validate resolver
|
||||
|
||||
const manager: Manager = {
|
||||
resolver: {
|
||||
setup(context: MetaContext) {
|
||||
if (!resolver || isFunction(resolver)) {
|
||||
return
|
||||
}
|
||||
|
||||
resolver.setup(context)
|
||||
},
|
||||
resolve(key: string, pathSegments: PathSegments, getShadow, getActive) {
|
||||
if (!resolver) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isFunction(resolver)) {
|
||||
return resolver(key, pathSegments, getShadow, getActive)
|
||||
}
|
||||
|
||||
return resolver.resolve(key, pathSegments, getShadow, getActive)
|
||||
},
|
||||
},
|
||||
config: {
|
||||
...defaultMapping,
|
||||
...options.config,
|
||||
},
|
||||
install(app: App) {
|
||||
applyMetaPlugin(app, this)
|
||||
},
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { isObject } from '@vue/shared'
|
||||
import { update } from './info/update'
|
||||
import { MetaContext, MetainfoInput, PathSegments } from './types'
|
||||
|
||||
interface Target extends MetainfoInput {
|
||||
__vm_proxy?: any
|
||||
}
|
||||
|
||||
export function createProxy(
|
||||
target: Target,
|
||||
handler: ProxyHandler<object>
|
||||
): Target {
|
||||
return markRaw(new Proxy(target, handler))
|
||||
}
|
||||
|
||||
export function createHandler(
|
||||
context: MetaContext,
|
||||
pathSegments: PathSegments = []
|
||||
): ProxyHandler<object> {
|
||||
return {
|
||||
get(target: object, key: string, receiver: object) {
|
||||
const value = Reflect.get(target, key, receiver)
|
||||
|
||||
if (!isObject(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (!value.__vm_proxy) {
|
||||
const keyPath: PathSegments = [...pathSegments, key]
|
||||
|
||||
const handler = /*#__PURE__*/ createHandler(context, keyPath)
|
||||
value.__vm_proxy = createProxy(value, handler)
|
||||
}
|
||||
|
||||
return value.__vm_proxy
|
||||
},
|
||||
set(
|
||||
target: object,
|
||||
key: string,
|
||||
value: unknown,
|
||||
receiver: object
|
||||
): boolean {
|
||||
update(context, pathSegments, key, value)
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
import { h, VNode } from 'vue'
|
||||
import { isArray } from '@vue/shared'
|
||||
import { getConfigKey } from './config'
|
||||
import { TODO } from './types'
|
||||
|
||||
export interface RenderContext {
|
||||
slots: any
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export interface GroupConfig {
|
||||
group: string
|
||||
data: Array<TODO> | TODO
|
||||
tagNamespace?: string
|
||||
fullName?: string
|
||||
slotName?: string
|
||||
}
|
||||
|
||||
export interface SlotScopeProperties {
|
||||
content: any
|
||||
metainfo: any
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type RenderedMetainfoNode = {
|
||||
vnode: VNode
|
||||
target?: string
|
||||
}
|
||||
|
||||
export type RenderedMetainfo = Array<RenderedMetainfoNode>
|
||||
|
||||
export function renderMeta(
|
||||
context: RenderContext,
|
||||
key: string,
|
||||
data: TODO,
|
||||
config: TODO
|
||||
): RenderedMetainfo | RenderedMetainfoNode {
|
||||
// console.info('renderMeta', key, data, config)
|
||||
|
||||
if (config.group) {
|
||||
return renderGroup(context, key, data, config)
|
||||
}
|
||||
|
||||
return renderTag(context, key, data, config)
|
||||
}
|
||||
|
||||
export function renderGroup(
|
||||
context: RenderContext,
|
||||
key: string,
|
||||
data: TODO,
|
||||
config: TODO
|
||||
): RenderedMetainfo | RenderedMetainfoNode {
|
||||
// console.info('renderGroup', key, data, config)
|
||||
|
||||
if (isArray(data)) {
|
||||
config.contentAttributes = getConfigKey(
|
||||
[key, config.tag],
|
||||
'contentAttributes',
|
||||
config
|
||||
)
|
||||
return data.map(_data => renderTag(context, key, _data, config)).flat()
|
||||
}
|
||||
|
||||
return Object.keys(data)
|
||||
.map(childKey => {
|
||||
const groupConfig: GroupConfig = {
|
||||
group: key,
|
||||
data,
|
||||
}
|
||||
|
||||
if (config.namespaced) {
|
||||
groupConfig.tagNamespace =
|
||||
config.namespaced === true ? key : config.namespaced
|
||||
} else if (config.namespacedAttribute) {
|
||||
const namespace =
|
||||
config.namespacedAttribute === true ? key : config.namespacedAttribute
|
||||
|
||||
groupConfig.fullName = `${namespace}:${childKey}`
|
||||
groupConfig.slotName = `${namespace}(${childKey})`
|
||||
}
|
||||
|
||||
return renderTag(context, key, data[childKey], config, groupConfig)
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
|
||||
export function renderTag(
|
||||
context: RenderContext,
|
||||
key: string,
|
||||
data: TODO,
|
||||
config: TODO = {},
|
||||
groupConfig?: GroupConfig
|
||||
): RenderedMetainfo | RenderedMetainfoNode {
|
||||
if (!config.group && isArray(data)) {
|
||||
data = { content: data }
|
||||
}
|
||||
|
||||
let content, hasChilds
|
||||
|
||||
if (isArray(data)) {
|
||||
return data
|
||||
.map(child => {
|
||||
return renderTag(context, key, child, config, groupConfig)
|
||||
})
|
||||
.flat()
|
||||
} else if (data.content && isArray(data.content)) {
|
||||
content = data.content.map((child: string | TODO) => {
|
||||
if (typeof child === 'string') {
|
||||
return child
|
||||
}
|
||||
return renderTag(context, key, child, config, groupConfig)
|
||||
})
|
||||
hasChilds = true
|
||||
} else {
|
||||
content = data
|
||||
}
|
||||
|
||||
const { tag = config.tag || key } = data
|
||||
|
||||
const fullName = (groupConfig && groupConfig.fullName) || key
|
||||
const slotName = (groupConfig && groupConfig.slotName) || key
|
||||
|
||||
let { attrs: attributes } = data
|
||||
if (!attributes && typeof data === 'object') {
|
||||
attributes = { ...data }
|
||||
|
||||
delete attributes.tag
|
||||
delete attributes.content
|
||||
delete attributes.target
|
||||
} else {
|
||||
attributes = {}
|
||||
}
|
||||
|
||||
if (hasChilds) {
|
||||
content = getSlotContent(context, slotName, content, config, data)
|
||||
} else {
|
||||
const contentAttributes = getConfigKey(tag, 'contentAttributes', config)
|
||||
|
||||
if (contentAttributes) {
|
||||
if (!config.nameless) {
|
||||
const nameAttribute = getConfigKey(tag, 'nameAttribute', config)
|
||||
if (nameAttribute) {
|
||||
attributes[nameAttribute] = fullName
|
||||
}
|
||||
}
|
||||
|
||||
const contentAttribute = config.contentAttribute || contentAttributes[0]
|
||||
attributes[contentAttribute] = getSlotContent(
|
||||
context,
|
||||
slotName,
|
||||
attributes[contentAttribute] || content,
|
||||
config,
|
||||
groupConfig
|
||||
)
|
||||
content = undefined
|
||||
} else {
|
||||
content = getSlotContent(context, slotName, content, config, data)
|
||||
}
|
||||
}
|
||||
|
||||
const finalTag =
|
||||
groupConfig && groupConfig.tagNamespace
|
||||
? `${groupConfig.tagNamespace}:${tag}`
|
||||
: tag
|
||||
|
||||
// console.info('FINAL TAG', finalTag)
|
||||
// console.log(' ATTRIBUTES', attributes)
|
||||
// console.log(' CONTENT', content)
|
||||
// // console.log(data, attributes, config)
|
||||
|
||||
if (hasChilds) {
|
||||
for (const child of content) {
|
||||
if (typeof child === 'string') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (child.type === finalTag) {
|
||||
return content
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const vnode = h(finalTag, attributes, content)
|
||||
|
||||
return {
|
||||
target: data.target,
|
||||
vnode,
|
||||
}
|
||||
}
|
||||
|
||||
export function getSlotContent(
|
||||
{ metainfo, slots }: RenderContext,
|
||||
slotName: string,
|
||||
content: any,
|
||||
config: TODO,
|
||||
groupConfig?: GroupConfig
|
||||
): TODO {
|
||||
if (!slots[slotName]) {
|
||||
return content
|
||||
}
|
||||
|
||||
const slotProps: SlotScopeProperties = {
|
||||
content,
|
||||
metainfo,
|
||||
}
|
||||
|
||||
if (groupConfig && groupConfig.group) {
|
||||
slotProps[groupConfig.group] = groupConfig.data
|
||||
}
|
||||
|
||||
content = slots[slotName](slotProps)
|
||||
|
||||
if (content.length) {
|
||||
return content[0].children
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
ActiveNode,
|
||||
/*ActiveResolverSetup, ActiveResolverMethod,*/ MetaContext,
|
||||
PathSegments,
|
||||
ShadowNode,
|
||||
} from '../types'
|
||||
|
||||
export function setup(context: MetaContext): void {}
|
||||
|
||||
export function resolve(
|
||||
key: string,
|
||||
pathSegments: PathSegments,
|
||||
shadow: ShadowNode,
|
||||
active: ActiveNode
|
||||
): any {}
|
||||
@@ -0,0 +1 @@
|
||||
export interface Resolver {}
|
||||
@@ -1,12 +0,0 @@
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import { serverSequences } from '../shared/escaping'
|
||||
import { setOptions } from '../shared/options'
|
||||
import generateServerInjector from './generateServerInjector'
|
||||
|
||||
export default function generate (rawInfo, options) {
|
||||
options = setOptions(options)
|
||||
const metaInfo = getMetaInfo(options, rawInfo, serverSequences)
|
||||
|
||||
const serverInjector = generateServerInjector(options, metaInfo)
|
||||
return serverInjector.injectors
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { metaInfoOptionKeys, metaInfoAttributeKeys, defaultInfo } from '../shared/constants'
|
||||
import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
|
||||
|
||||
/**
|
||||
* Converts a meta info property to one that can be stringified on the server
|
||||
*
|
||||
* @param {String} type - the type of data to convert
|
||||
* @param {(String|Object|Array<Object>)} data - the data value
|
||||
* @return {Object} - the new injector
|
||||
*/
|
||||
|
||||
export default function generateServerInjector (options, metaInfo) {
|
||||
const serverInjector = {
|
||||
data: metaInfo,
|
||||
extraData: undefined,
|
||||
addInfo (appId, metaInfo) {
|
||||
this.extraData = this.extraData || {}
|
||||
this.extraData[appId] = metaInfo
|
||||
},
|
||||
callInjectors (opts) {
|
||||
const m = this.injectors
|
||||
|
||||
// only call title for the head
|
||||
return (opts.body || opts.pbody ? '' : m.title.text(opts)) +
|
||||
m.meta.text(opts) +
|
||||
m.link.text(opts) +
|
||||
m.style.text(opts) +
|
||||
m.script.text(opts) +
|
||||
m.noscript.text(opts)
|
||||
},
|
||||
injectors: {
|
||||
head: ln => serverInjector.callInjectors({ ln }),
|
||||
bodyPrepend: ln => serverInjector.callInjectors({ ln, pbody: true }),
|
||||
bodyAppend: ln => serverInjector.callInjectors({ ln, body: true })
|
||||
}
|
||||
}
|
||||
|
||||
for (const type in defaultInfo) {
|
||||
if (metaInfoOptionKeys.includes(type)) {
|
||||
continue
|
||||
}
|
||||
|
||||
serverInjector.injectors[type] = {
|
||||
text (arg) {
|
||||
if (type === 'title') {
|
||||
return titleGenerator(options, type, serverInjector.data[type], arg)
|
||||
}
|
||||
|
||||
if (metaInfoAttributeKeys.includes(type)) {
|
||||
const attributeData = {}
|
||||
|
||||
const data = serverInjector.data[type]
|
||||
if (data) {
|
||||
for (const attr in data) {
|
||||
attributeData[attr] = {
|
||||
[options.ssrAppId]: data[attr]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serverInjector.extraData) {
|
||||
for (const appId in serverInjector.extraData) {
|
||||
const data = serverInjector.extraData[appId][type]
|
||||
if (data) {
|
||||
for (const attr in data) {
|
||||
attributeData[attr] = {
|
||||
...attributeData[attr],
|
||||
[appId]: data[attr]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributeGenerator(options, type, attributeData, arg)
|
||||
}
|
||||
|
||||
let str = tagGenerator(options, type, serverInjector.data[type], arg)
|
||||
|
||||
if (serverInjector.extraData) {
|
||||
for (const appId in serverInjector.extraData) {
|
||||
const data = serverInjector.extraData[appId][type]
|
||||
const extraStr = tagGenerator(options, type, data, { appId, ...arg })
|
||||
str = `${str}${extraStr}`
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return serverInjector
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { booleanHtmlAttributes } from '../../shared/constants'
|
||||
|
||||
/**
|
||||
* Generates tag attributes for use on the server.
|
||||
*
|
||||
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
|
||||
* @param {Object} data - the attributes to generate
|
||||
* @return {Object} - the attribute generator
|
||||
*/
|
||||
export default function attributeGenerator (options, type, data, addSrrAttribute) {
|
||||
const { attribute, ssrAttribute } = options || {}
|
||||
let attributeStr = ''
|
||||
|
||||
for (const attr in data) {
|
||||
const attrData = data[attr]
|
||||
const attrValues = []
|
||||
|
||||
for (const appId in attrData) {
|
||||
attrValues.push(...[].concat(attrData[appId]))
|
||||
}
|
||||
|
||||
if (attrValues.length) {
|
||||
attributeStr += booleanHtmlAttributes.includes(attr) && attrValues.some(Boolean)
|
||||
? `${attr}`
|
||||
: `${attr}="${attrValues.join(' ')}"`
|
||||
|
||||
attributeStr += ' '
|
||||
}
|
||||
}
|
||||
|
||||
if (attributeStr) {
|
||||
attributeStr += `${attribute}="${encodeURI(JSON.stringify(data))}"`
|
||||
}
|
||||
|
||||
if (type === 'htmlAttrs' && addSrrAttribute) {
|
||||
return `${ssrAttribute}${attributeStr ? ' ' : ''}${attributeStr}`
|
||||
}
|
||||
|
||||
return attributeStr
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as attributeGenerator } from './attribute'
|
||||
export { default as titleGenerator } from './title'
|
||||
export { default as tagGenerator } from './tag'
|
||||
@@ -1,92 +0,0 @@
|
||||
import {
|
||||
booleanHtmlAttributes,
|
||||
tagsWithoutEndTag,
|
||||
tagsWithInnerContent,
|
||||
tagAttributeAsInnerContent,
|
||||
tagProperties,
|
||||
commonDataAttributes
|
||||
} from '../../shared/constants'
|
||||
|
||||
/**
|
||||
* Generates meta, base, link, style, script, noscript tags for use on the server
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - the tag generator
|
||||
*/
|
||||
export default function tagGenerator (options, type, tags, generatorOptions) {
|
||||
const { ssrAppId, attribute, tagIDKeyName } = options || {}
|
||||
const { appId, body = false, pbody = false, ln = false } = generatorOptions || {}
|
||||
|
||||
const dataAttributes = [tagIDKeyName, ...commonDataAttributes]
|
||||
|
||||
if (!tags || !tags.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// build a string containing all tags of this type
|
||||
return tags.reduce((tagsStr, tag) => {
|
||||
if (tag.skip) {
|
||||
return tagsStr
|
||||
}
|
||||
|
||||
const tagKeys = Object.keys(tag)
|
||||
|
||||
if (tagKeys.length === 0) {
|
||||
return tagsStr // Bail on empty tag object
|
||||
}
|
||||
|
||||
if (Boolean(tag.body) !== body || Boolean(tag.pbody) !== pbody) {
|
||||
return tagsStr
|
||||
}
|
||||
|
||||
let attrs = tag.once ? '' : ` ${attribute}="${appId || ssrAppId}"`
|
||||
|
||||
// build a string containing all attributes of this tag
|
||||
for (const attr in tag) {
|
||||
// these attributes are treated as children on the tag
|
||||
if (tagAttributeAsInnerContent.includes(attr) || tagProperties.includes(attr)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (attr === 'callback') {
|
||||
attrs += ' onload="this.__vm_l=1"'
|
||||
continue
|
||||
}
|
||||
|
||||
// these form the attribute list for this tag
|
||||
let prefix = ''
|
||||
if (dataAttributes.includes(attr)) {
|
||||
prefix = 'data-'
|
||||
}
|
||||
|
||||
const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr)
|
||||
if (isBooleanAttr && !tag[attr]) {
|
||||
continue
|
||||
}
|
||||
|
||||
attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
|
||||
}
|
||||
|
||||
let json = ''
|
||||
if (tag.json) {
|
||||
json = JSON.stringify(tag.json)
|
||||
}
|
||||
|
||||
// grab child content from one of these attributes, if possible
|
||||
const content = tag.innerHTML || tag.cssText || json
|
||||
|
||||
// generate tag exactly without any other redundant attribute
|
||||
|
||||
// these tags have no end tag
|
||||
const hasEndTag = !tagsWithoutEndTag.includes(type)
|
||||
|
||||
// these tag types will have content inserted
|
||||
const hasContent = hasEndTag && tagsWithInnerContent.includes(type)
|
||||
|
||||
// the final string for this specific tag
|
||||
return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` +
|
||||
(hasContent ? `${content}</${type}>` : '') +
|
||||
(ln ? '\n' : '')
|
||||
}, '')
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Generates title output for the server
|
||||
*
|
||||
* @param {'title'} type - the string "title"
|
||||
* @param {String} data - the title text
|
||||
* @return {Object} - the title generator
|
||||
*/
|
||||
export default function titleGenerator (options, type, data, generatorOptions) {
|
||||
const { ln } = generatorOptions || {}
|
||||
|
||||
if (!data) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `<${type}>${data}</${type}>${ln ? '\n' : ''}`
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { serverSequences } from '../shared/escaping'
|
||||
import { rootConfigKey } from '../shared/constants'
|
||||
import { showWarningNotSupported } from '../shared/log'
|
||||
import { getComponentMetaInfo } from '../shared/getComponentOption'
|
||||
import { getAppsMetaInfo, clearAppsMetaInfo } from '../shared/additional-app'
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import generateServerInjector from './generateServerInjector'
|
||||
|
||||
/**
|
||||
* Converts the state of the meta info object such that each item
|
||||
* can be compiled to a tag string on the server
|
||||
*
|
||||
* @vm {Object} - Vue instance - ideally the root component
|
||||
* @return {Object} - server meta info with `toString` methods
|
||||
*/
|
||||
export default function inject (rootVm, options) {
|
||||
// make sure vue-meta was initiated
|
||||
if (!rootVm[rootConfigKey]) {
|
||||
showWarningNotSupported()
|
||||
return {}
|
||||
}
|
||||
|
||||
// collect & aggregate all metaInfo $options
|
||||
const rawInfo = getComponentMetaInfo(options, rootVm)
|
||||
|
||||
const metaInfo = getMetaInfo(options, rawInfo, serverSequences, rootVm)
|
||||
|
||||
// generate server injector
|
||||
const serverInjector = generateServerInjector(options, metaInfo)
|
||||
|
||||
// add meta info from additional apps
|
||||
const appsMetaInfo = getAppsMetaInfo()
|
||||
if (appsMetaInfo) {
|
||||
for (const additionalAppId in appsMetaInfo) {
|
||||
serverInjector.addInfo(additionalAppId, appsMetaInfo[additionalAppId])
|
||||
delete appsMetaInfo[additionalAppId]
|
||||
}
|
||||
clearAppsMetaInfo(true)
|
||||
}
|
||||
|
||||
return serverInjector.injectors
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import refresh from '../client/refresh'
|
||||
import inject from '../server/inject'
|
||||
import { addApp } from './additional-app'
|
||||
import { showWarningNotSupportedInBrowserBundle } from './log'
|
||||
import { addNavGuards } from './nav-guards'
|
||||
import { pause, resume } from './pausing'
|
||||
import { getOptions } from './options'
|
||||
|
||||
export default function $meta (options) {
|
||||
options = options || {}
|
||||
/**
|
||||
* Returns an injector for server-side rendering.
|
||||
* @this {Object} - the Vue instance (a root component)
|
||||
* @return {Object} - injector
|
||||
*/
|
||||
const $root = this.$root
|
||||
|
||||
return {
|
||||
getOptions: () => getOptions(options),
|
||||
setOptions: (newOptions) => {
|
||||
const refreshNavKey = 'refreshOnceOnNavigation'
|
||||
if (newOptions && newOptions[refreshNavKey]) {
|
||||
options.refreshOnceOnNavigation = !!newOptions[refreshNavKey]
|
||||
addNavGuards($root)
|
||||
}
|
||||
|
||||
const debounceWaitKey = 'debounceWait'
|
||||
if (newOptions && debounceWaitKey in newOptions) {
|
||||
const debounceWait = parseInt(newOptions[debounceWaitKey])
|
||||
if (!isNaN(debounceWait)) {
|
||||
options.debounceWait = debounceWait
|
||||
}
|
||||
}
|
||||
|
||||
const waitOnDestroyedKey = 'waitOnDestroyed'
|
||||
if (newOptions && waitOnDestroyedKey in newOptions) {
|
||||
options.waitOnDestroyed = !!newOptions[waitOnDestroyedKey]
|
||||
}
|
||||
},
|
||||
refresh: () => refresh($root, options),
|
||||
inject: () => process.server ? inject($root, options) : showWarningNotSupportedInBrowserBundle('inject'),
|
||||
pause: () => pause($root),
|
||||
resume: () => resume($root),
|
||||
addApp: appId => addApp($root, appId, options)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import updateClientMetaInfo from '../client/updateClientMetaInfo'
|
||||
import { updateAttribute } from '../client/updaters'
|
||||
import { metaInfoAttributeKeys } from '../shared/constants'
|
||||
import { getTag, removeElementsByAppId } from '../utils/elements'
|
||||
|
||||
let appsMetaInfo
|
||||
|
||||
export function addApp (rootVm, appId, options) {
|
||||
return {
|
||||
set: metaInfo => setMetaInfo(rootVm, appId, options, metaInfo),
|
||||
remove: () => removeMetaInfo(rootVm, appId, options)
|
||||
}
|
||||
}
|
||||
|
||||
export function setMetaInfo (rootVm, appId, options, metaInfo) {
|
||||
// if a vm exists _and_ its mounted then immediately update
|
||||
if (rootVm && rootVm.$el) {
|
||||
return updateClientMetaInfo(appId, options, metaInfo)
|
||||
}
|
||||
|
||||
// store for later, the info
|
||||
// will be set on the first refresh
|
||||
appsMetaInfo = appsMetaInfo || {}
|
||||
appsMetaInfo[appId] = metaInfo
|
||||
}
|
||||
|
||||
export function removeMetaInfo (rootVm, appId, options) {
|
||||
if (rootVm && rootVm.$el) {
|
||||
const tags = {}
|
||||
for (const type of metaInfoAttributeKeys) {
|
||||
const tagName = type.substr(0, 4)
|
||||
updateAttribute(appId, options, type, {}, getTag(tags, tagName))
|
||||
}
|
||||
|
||||
return removeElementsByAppId(options, appId)
|
||||
}
|
||||
|
||||
if (appsMetaInfo[appId]) {
|
||||
delete appsMetaInfo[appId]
|
||||
clearAppsMetaInfo()
|
||||
}
|
||||
}
|
||||
|
||||
export function getAppsMetaInfo () {
|
||||
return appsMetaInfo
|
||||
}
|
||||
|
||||
export function clearAppsMetaInfo (force) {
|
||||
if (force || !Object.keys(appsMetaInfo).length) {
|
||||
appsMetaInfo = undefined
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* These are constant variables used throughout the application.
|
||||
*/
|
||||
|
||||
// set some sane defaults
|
||||
export const defaultInfo = {
|
||||
title: undefined,
|
||||
titleChunk: '',
|
||||
titleTemplate: '%s',
|
||||
htmlAttrs: {},
|
||||
bodyAttrs: {},
|
||||
headAttrs: {},
|
||||
base: [],
|
||||
link: [],
|
||||
meta: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
}
|
||||
|
||||
export const rootConfigKey = '_vueMeta'
|
||||
|
||||
// This is the name of the component option that contains all the information that
|
||||
// gets converted to the various meta tags & attributes for the page.
|
||||
export const keyName = 'metaInfo'
|
||||
|
||||
// This is the attribute vue-meta arguments on elements to know which it should
|
||||
// manage and which it should ignore.
|
||||
export const attribute = 'data-vue-meta'
|
||||
|
||||
// This is the attribute that goes on the `html` tag to inform `vue-meta`
|
||||
// that the server has already generated the meta tags for the initial render.
|
||||
export const ssrAttribute = 'data-vue-meta-server-rendered'
|
||||
|
||||
// This is the property that tells vue-meta to overwrite (instead of append)
|
||||
// an item in a tag list. For example, if you have two `meta` tag list items
|
||||
// that both have `vmid` of "description", then vue-meta will overwrite the
|
||||
// shallowest one with the deepest one.
|
||||
export const tagIDKeyName = 'vmid'
|
||||
|
||||
// This is the key name for possible meta templates
|
||||
export const metaTemplateKeyName = 'template'
|
||||
|
||||
// This is the key name for the content-holding property
|
||||
export const contentKeyName = 'content'
|
||||
|
||||
// The id used for the ssr app
|
||||
export const ssrAppId = 'ssr'
|
||||
|
||||
// How long meta update
|
||||
export const debounceWait = 10
|
||||
|
||||
// How long meta update
|
||||
export const waitOnDestroyed = true
|
||||
|
||||
export const defaultOptions = {
|
||||
keyName,
|
||||
attribute,
|
||||
ssrAttribute,
|
||||
tagIDKeyName,
|
||||
contentKeyName,
|
||||
metaTemplateKeyName,
|
||||
waitOnDestroyed,
|
||||
debounceWait,
|
||||
ssrAppId
|
||||
}
|
||||
|
||||
// might be a bit ugly, but minimizes the browser bundles a bit
|
||||
const defaultInfoKeys = Object.keys(defaultInfo)
|
||||
|
||||
// The metaInfo property keys which are used to disable escaping
|
||||
export const disableOptionKeys = [
|
||||
defaultInfoKeys[12],
|
||||
defaultInfoKeys[13]
|
||||
]
|
||||
|
||||
// List of metaInfo property keys which are configuration options (and dont generate html)
|
||||
export const metaInfoOptionKeys = [
|
||||
defaultInfoKeys[1],
|
||||
defaultInfoKeys[2],
|
||||
'changed',
|
||||
...disableOptionKeys
|
||||
]
|
||||
|
||||
// List of metaInfo property keys which only generates attributes and no tags
|
||||
export const metaInfoAttributeKeys = [
|
||||
defaultInfoKeys[3],
|
||||
defaultInfoKeys[4],
|
||||
defaultInfoKeys[5]
|
||||
]
|
||||
|
||||
// HTML elements which support the onload event
|
||||
export const tagsSupportingOnload = ['link', 'style', 'script']
|
||||
|
||||
// HTML elements which dont have a head tag (shortened to our needs)
|
||||
// see: https://www.w3.org/TR/html52/document-metadata.html
|
||||
export const tagsWithoutEndTag = ['base', 'meta', 'link']
|
||||
|
||||
// HTML elements which can have inner content (shortened to our needs)
|
||||
export const tagsWithInnerContent = ['noscript', 'script', 'style']
|
||||
|
||||
// Attributes which are inserted as childNodes instead of HTMLAttribute
|
||||
export const tagAttributeAsInnerContent = ['innerHTML', 'cssText', 'json']
|
||||
|
||||
export const tagProperties = ['once', 'skip', 'template']
|
||||
|
||||
// Attributes which should be added with data- prefix
|
||||
export const commonDataAttributes = ['body', 'pbody']
|
||||
|
||||
// from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202
|
||||
export const booleanHtmlAttributes = [
|
||||
'allowfullscreen',
|
||||
'amp',
|
||||
'amp-boilerplate',
|
||||
'async',
|
||||
'autofocus',
|
||||
'autoplay',
|
||||
'checked',
|
||||
'compact',
|
||||
'controls',
|
||||
'declare',
|
||||
'default',
|
||||
'defaultchecked',
|
||||
'defaultmuted',
|
||||
'defaultselected',
|
||||
'defer',
|
||||
'disabled',
|
||||
'enabled',
|
||||
'formnovalidate',
|
||||
'hidden',
|
||||
'indeterminate',
|
||||
'inert',
|
||||
'ismap',
|
||||
'itemscope',
|
||||
'loop',
|
||||
'multiple',
|
||||
'muted',
|
||||
'nohref',
|
||||
'noresize',
|
||||
'noshade',
|
||||
'novalidate',
|
||||
'nowrap',
|
||||
'open',
|
||||
'pauseonexit',
|
||||
'readonly',
|
||||
'required',
|
||||
'reversed',
|
||||
'scoped',
|
||||
'seamless',
|
||||
'selected',
|
||||
'sortable',
|
||||
'truespeed',
|
||||
'typemustmatch',
|
||||
'visible'
|
||||
]
|
||||
@@ -1,108 +0,0 @@
|
||||
import { isString, isArray, isPureObject } from '../utils/is-type'
|
||||
import { includes } from '../utils/array'
|
||||
import { ensureIsArray } from '../utils/ensure'
|
||||
import { metaInfoOptionKeys, disableOptionKeys } from './constants'
|
||||
|
||||
export const serverSequences = [
|
||||
[/&/g, '&'],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, ''']
|
||||
]
|
||||
|
||||
export const clientSequences = [
|
||||
[/&/g, '\u0026'],
|
||||
[/</g, '\u003C'],
|
||||
[/>/g, '\u003E'],
|
||||
[/"/g, '\u0022'],
|
||||
[/'/g, '\u0027']
|
||||
]
|
||||
|
||||
// sanitizes potentially dangerous characters
|
||||
export function escape (info, options, escapeOptions, escapeKeys) {
|
||||
const { tagIDKeyName } = options
|
||||
const { doEscape = v => v } = escapeOptions
|
||||
const escaped = {}
|
||||
|
||||
for (const key in info) {
|
||||
const value = info[key]
|
||||
|
||||
// no need to escape configuration options
|
||||
if (includes(metaInfoOptionKeys, key)) {
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
// do not use destructuring for disableOptionKeys, it increases transpiled size
|
||||
// due to var checks while we are guaranteed the structure of the cb
|
||||
let disableKey = disableOptionKeys[0]
|
||||
|
||||
if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) {
|
||||
// this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
const tagId = info[tagIDKeyName]
|
||||
if (tagId) {
|
||||
disableKey = disableOptionKeys[1]
|
||||
|
||||
// keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped
|
||||
if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) {
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (isString(value)) {
|
||||
escaped[key] = doEscape(value)
|
||||
} else if (isArray(value)) {
|
||||
escaped[key] = value.map((v) => {
|
||||
if (isPureObject(v)) {
|
||||
return escape(v, options, escapeOptions, true)
|
||||
}
|
||||
|
||||
return doEscape(v)
|
||||
})
|
||||
} else if (isPureObject(value)) {
|
||||
escaped[key] = escape(value, options, escapeOptions, true)
|
||||
} else {
|
||||
escaped[key] = value
|
||||
}
|
||||
|
||||
if (escapeKeys) {
|
||||
const escapedKey = doEscape(key)
|
||||
if (key !== escapedKey) {
|
||||
escaped[escapedKey] = escaped[key]
|
||||
delete escaped[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return escaped
|
||||
}
|
||||
|
||||
export function escapeMetaInfo (options, info, escapeSequences) {
|
||||
escapeSequences = escapeSequences || []
|
||||
// do not use destructuring for seq, it increases transpiled size
|
||||
// due to var checks while we are guaranteed the structure of the cb
|
||||
const escapeOptions = {
|
||||
doEscape: value => escapeSequences.reduce((val, seq) => val.replace(seq[0], seq[1]), value)
|
||||
}
|
||||
|
||||
disableOptionKeys.forEach((disableKey, index) => {
|
||||
if (index === 0) {
|
||||
ensureIsArray(info, disableKey)
|
||||
} else if (index === 1) {
|
||||
for (const key in info[disableKey]) {
|
||||
ensureIsArray(info[disableKey], key)
|
||||
}
|
||||
}
|
||||
|
||||
escapeOptions[disableKey] = info[disableKey]
|
||||
})
|
||||
|
||||
// begin sanitization
|
||||
return escape(info, options, escapeOptions)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { isObject } from '../utils/is-type'
|
||||
import { defaultInfo } from './constants'
|
||||
import { merge } from './merge'
|
||||
import { inMetaInfoBranch } from './meta-helpers'
|
||||
|
||||
export function getComponentMetaInfo (options, component) {
|
||||
return getComponentOption(options || {}, component, defaultInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `opts.option` $option value of the given `opts.component`.
|
||||
* If methods are encountered, they will be bound to the component context.
|
||||
* If `opts.deep` is true, will recursively merge all child component
|
||||
* `opts.option` $option values into the returned result.
|
||||
*
|
||||
* @param {Object} opts - options
|
||||
* @param {Object} opts.component - Vue component to fetch option data from
|
||||
* @param {Boolean} opts.deep - look for data in child components as well?
|
||||
* @param {Function} opts.arrayMerge - how should arrays be merged?
|
||||
* @param {String} opts.keyName - the name of the option to look for
|
||||
* @param {Object} [result={}] - result so far
|
||||
* @return {Object} result - final aggregated result
|
||||
*/
|
||||
export function getComponentOption (options, component, result) {
|
||||
result = result || {}
|
||||
|
||||
if (component._inactive) {
|
||||
return result
|
||||
}
|
||||
|
||||
options = options || {}
|
||||
const { keyName } = options
|
||||
const { $metaInfo, $options, $children } = component
|
||||
|
||||
// only collect option data if it exists
|
||||
if ($options[keyName]) {
|
||||
// if $metaInfo exists then [keyName] was defined as a function
|
||||
// and set to the computed prop $metaInfo in the mixin
|
||||
// using the computed prop should be a small performance increase
|
||||
// because Vue caches those internally
|
||||
const data = $metaInfo || $options[keyName]
|
||||
|
||||
// only merge data with result when its an object
|
||||
// eg it could be a function when metaInfo() returns undefined
|
||||
// dueo to the or statement above
|
||||
if (isObject(data)) {
|
||||
result = merge(result, data, options)
|
||||
}
|
||||
}
|
||||
|
||||
// collect & aggregate child options if deep = true
|
||||
if ($children.length) {
|
||||
$children.forEach((childComponent) => {
|
||||
// check if the childComponent is in a branch
|
||||
// return otherwise so we dont walk all component branches unnecessarily
|
||||
if (!inMetaInfoBranch(childComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
result = getComponentOption(options, childComponent, result)
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { findIndex } from '../utils/array'
|
||||
import { escapeMetaInfo } from '../shared/escaping'
|
||||
import { applyTemplate } from './template'
|
||||
|
||||
/**
|
||||
* Returns the correct meta info for the given component
|
||||
* (child components will overwrite parent meta info)
|
||||
*
|
||||
* @param {Object} component - the Vue instance to get meta info from
|
||||
* @return {Object} - returned meta info
|
||||
*/
|
||||
export default function getMetaInfo (options, info, escapeSequences, component) {
|
||||
options = options || {}
|
||||
escapeSequences = escapeSequences || []
|
||||
|
||||
const { tagIDKeyName } = options
|
||||
// Remove all "template" tags from meta
|
||||
|
||||
// backup the title chunk in case user wants access to it
|
||||
if (info.title) {
|
||||
info.titleChunk = info.title
|
||||
}
|
||||
|
||||
// replace title with populated template
|
||||
if (info.titleTemplate && info.titleTemplate !== '%s') {
|
||||
applyTemplate({ component, contentKeyName: 'title' }, info, info.titleTemplate, info.titleChunk || '')
|
||||
}
|
||||
|
||||
// convert base tag to an array so it can be handled the same way
|
||||
// as the other tags
|
||||
if (info.base) {
|
||||
info.base = Object.keys(info.base).length ? [info.base] : []
|
||||
}
|
||||
|
||||
if (info.meta) {
|
||||
// remove meta items with duplicate vmid's
|
||||
info.meta = info.meta.filter((metaItem, index, arr) => {
|
||||
const hasVmid = !!metaItem[tagIDKeyName]
|
||||
if (!hasVmid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isFirstItemForVmid = index === findIndex(arr, item => item[tagIDKeyName] === metaItem[tagIDKeyName])
|
||||
return isFirstItemForVmid
|
||||
})
|
||||
|
||||
// apply templates if needed
|
||||
info.meta.forEach(metaObject => applyTemplate(options, metaObject))
|
||||
}
|
||||
|
||||
return escapeMetaInfo(options, info, escapeSequences)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { hasGlobalWindow } from '../utils/window'
|
||||
|
||||
const _global = hasGlobalWindow ? window : global
|
||||
|
||||
const console = _global.console || {}
|
||||
|
||||
export function warn (str) {
|
||||
/* istanbul ignore next */
|
||||
if (!console || !console.warn) {
|
||||
return
|
||||
}
|
||||
|
||||
console.warn(str)
|
||||
}
|
||||
|
||||
export const showWarningNotSupportedInBrowserBundle = method => warn(`${method} is not supported in browser builds`)
|
||||
|
||||
export const showWarningNotSupported = () => warn('This vue app/component has no vue-meta configuration')
|
||||
@@ -1,111 +0,0 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import { includes, findIndex } from '../utils/array'
|
||||
import { applyTemplate } from './template'
|
||||
import { metaInfoAttributeKeys, booleanHtmlAttributes } from './constants'
|
||||
import { warn } from './log'
|
||||
|
||||
export function arrayMerge ({ component, tagIDKeyName, metaTemplateKeyName, contentKeyName }, target, source) {
|
||||
// we concat the arrays without merging objects contained in,
|
||||
// but we check for a `vmid` property on each object in the array
|
||||
// using an O(1) lookup associative array exploit
|
||||
const destination = []
|
||||
|
||||
if (!target.length && !source.length) {
|
||||
return destination
|
||||
}
|
||||
|
||||
target.forEach((targetItem, targetIndex) => {
|
||||
// no tagID so no need to check for duplicity
|
||||
if (!targetItem[tagIDKeyName]) {
|
||||
destination.push(targetItem)
|
||||
return
|
||||
}
|
||||
|
||||
const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName])
|
||||
const sourceItem = source[sourceIndex]
|
||||
|
||||
// source doesnt contain any duplicate vmid's, we can keep targetItem
|
||||
if (sourceIndex === -1) {
|
||||
destination.push(targetItem)
|
||||
return
|
||||
}
|
||||
|
||||
// when sourceItem explictly defines contentKeyName or innerHTML as undefined, its
|
||||
// an indication that we need to skip the default behaviour or child has preference over parent
|
||||
// which means we keep the targetItem and ignore/remove the sourceItem
|
||||
if (
|
||||
(contentKeyName in sourceItem && sourceItem[contentKeyName] === undefined) ||
|
||||
('innerHTML' in sourceItem && sourceItem.innerHTML === undefined)
|
||||
) {
|
||||
destination.push(targetItem)
|
||||
// remove current index from source array so its not concatenated to destination below
|
||||
source.splice(sourceIndex, 1)
|
||||
return
|
||||
}
|
||||
|
||||
// we now know that targetItem is a duplicate and we should ignore it in favor of sourceItem
|
||||
|
||||
// if source specifies null as content then ignore both the target as the source
|
||||
if (sourceItem[contentKeyName] === null || sourceItem.innerHTML === null) {
|
||||
// remove current index from source array so its not concatenated to destination below
|
||||
source.splice(sourceIndex, 1)
|
||||
return
|
||||
}
|
||||
|
||||
// now we only need to check if the target has a template to combine it with the source
|
||||
const targetTemplate = targetItem[metaTemplateKeyName]
|
||||
if (!targetTemplate) {
|
||||
return
|
||||
}
|
||||
|
||||
const sourceTemplate = sourceItem[metaTemplateKeyName]
|
||||
if (!sourceTemplate) {
|
||||
// use parent template and child content
|
||||
applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, targetTemplate)
|
||||
|
||||
// set template to true to indicate template was already applied
|
||||
sourceItem.template = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!sourceItem[contentKeyName]) {
|
||||
// use parent content and child template
|
||||
applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, undefined, targetItem[contentKeyName])
|
||||
}
|
||||
})
|
||||
|
||||
return destination.concat(source)
|
||||
}
|
||||
|
||||
let warningShown = false
|
||||
|
||||
export function merge (target, source, options) {
|
||||
options = options || {}
|
||||
|
||||
// remove properties explicitly set to false so child components can
|
||||
// optionally _not_ overwrite the parents content
|
||||
// (for array properties this is checked in arrayMerge)
|
||||
if (source.title === undefined) {
|
||||
delete source.title
|
||||
}
|
||||
|
||||
metaInfoAttributeKeys.forEach((attrKey) => {
|
||||
if (!source[attrKey]) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const key in source[attrKey]) {
|
||||
if (key in source[attrKey] && source[attrKey][key] === undefined) {
|
||||
if (includes(booleanHtmlAttributes, key) && !warningShown) {
|
||||
warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details')
|
||||
warningShown = true
|
||||
}
|
||||
delete source[attrKey][key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return deepmerge(target, source, {
|
||||
arrayMerge: (t, s) => arrayMerge(options, t, s)
|
||||
})
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { isUndefined, isObject } from '../utils/is-type'
|
||||
import { rootConfigKey } from './constants'
|
||||
|
||||
// Vue $root instance has a _vueMeta object property, otherwise its a boolean true
|
||||
export function hasMetaInfo (vm) {
|
||||
vm = vm || this
|
||||
return vm && (vm[rootConfigKey] === true || isObject(vm[rootConfigKey]))
|
||||
}
|
||||
|
||||
// a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has
|
||||
export function inMetaInfoBranch (vm) {
|
||||
vm = vm || this
|
||||
return vm && !isUndefined(vm[rootConfigKey])
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import { triggerUpdate } from '../client/update'
|
||||
import { isUndefined, isFunction } from '../utils/is-type'
|
||||
import { find } from '../utils/array'
|
||||
import { rootConfigKey } from './constants'
|
||||
import { hasMetaInfo } from './meta-helpers'
|
||||
import { addNavGuards } from './nav-guards'
|
||||
import { warn } from './log'
|
||||
|
||||
let appId = 1
|
||||
|
||||
export default function createMixin (Vue, options) {
|
||||
// for which Vue lifecycle hooks should the metaInfo be refreshed
|
||||
const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']
|
||||
|
||||
// watch for client side component updates
|
||||
return {
|
||||
beforeCreate () {
|
||||
const rootKey = '$root'
|
||||
const $root = this[rootKey]
|
||||
const $options = this.$options
|
||||
const devtoolsEnabled = Vue.config.devtools
|
||||
|
||||
Object.defineProperty(this, '_hasMetaInfo', {
|
||||
configurable: true,
|
||||
get () {
|
||||
// Show deprecation warning once when devtools enabled
|
||||
if (devtoolsEnabled && !$root[rootConfigKey].deprecationWarningShown) {
|
||||
warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead')
|
||||
$root[rootConfigKey].deprecationWarningShown = true
|
||||
}
|
||||
return hasMetaInfo(this)
|
||||
}
|
||||
})
|
||||
|
||||
// Add a marker to know if it uses metaInfo
|
||||
// _vnode is used to know that it's attached to a real component
|
||||
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
|
||||
if (isUndefined($options[options.keyName]) || $options[options.keyName] === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$root[rootConfigKey]) {
|
||||
$root[rootConfigKey] = { appId }
|
||||
appId++
|
||||
|
||||
if (devtoolsEnabled && $root.$options[options.keyName]) {
|
||||
// use nextTick so the children should be added to $root
|
||||
this.$nextTick(() => {
|
||||
// find the first child that lists fnOptions
|
||||
const child = find($root.$children, c => c.$vnode && c.$vnode.fnOptions)
|
||||
if (child && child.$vnode.fnOptions[options.keyName]) {
|
||||
warn(`VueMeta has detected a possible global mixin which adds a ${options.keyName} property to all Vue components on the page. This could cause severe performance issues. If possible, use $meta().addApp to add meta information instead`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// to speed up updates we keep track of branches which have a component with vue-meta info defined
|
||||
// if _vueMeta = true it has info, if _vueMeta = false a child has info
|
||||
if (!this[rootConfigKey]) {
|
||||
this[rootConfigKey] = true
|
||||
|
||||
let parent = this.$parent
|
||||
while (parent && parent !== $root) {
|
||||
if (isUndefined(parent[rootConfigKey])) {
|
||||
parent[rootConfigKey] = false
|
||||
}
|
||||
parent = parent.$parent
|
||||
}
|
||||
}
|
||||
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
if (isFunction($options[options.keyName])) {
|
||||
$options.computed = $options.computed || {}
|
||||
$options.computed.$metaInfo = $options[options.keyName]
|
||||
|
||||
if (!this.$isServer) {
|
||||
// if computed $metaInfo exists, watch it for updates & trigger a refresh
|
||||
// when it changes (i.e. automatically handle async actions that affect metaInfo)
|
||||
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
|
||||
this.$on('hook:created', function () {
|
||||
this.$watch('$metaInfo', function () {
|
||||
triggerUpdate(options, this[rootKey], 'watcher')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// force an initial refresh on page load and prevent other lifecycleHooks
|
||||
// to triggerUpdate until this initial refresh is finished
|
||||
// this is to make sure that when a page is opened in an inactive tab which
|
||||
// has throttled rAF/timers we still immediately set the page title
|
||||
if (isUndefined($root[rootConfigKey].initialized)) {
|
||||
$root[rootConfigKey].initialized = this.$isServer
|
||||
|
||||
if (!$root[rootConfigKey].initialized) {
|
||||
if (!$root[rootConfigKey].initializedSsr) {
|
||||
$root[rootConfigKey].initializedSsr = true
|
||||
|
||||
this.$on('hook:beforeMount', function () {
|
||||
const $root = this
|
||||
// if this Vue-app was server rendered, set the appId to 'ssr'
|
||||
// only one SSR app per page is supported
|
||||
if ($root.$el && $root.$el.nodeType === 1 && $root.$el.hasAttribute('data-server-rendered')) {
|
||||
$root[rootConfigKey].appId = options.ssrAppId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// we use the mounted hook here as on page load
|
||||
this.$on('hook:mounted', function () {
|
||||
const $root = this[rootKey]
|
||||
|
||||
if (!$root[rootConfigKey].initialized) {
|
||||
// used in triggerUpdate to check if a change was triggered
|
||||
// during initialization
|
||||
$root[rootConfigKey].initializing = true
|
||||
|
||||
// refresh meta in nextTick so all child components have loaded
|
||||
this.$nextTick(function () {
|
||||
const { tags, metaInfo } = $root.$meta().refresh()
|
||||
|
||||
// After ssr hydration (identifier by tags === false) check
|
||||
// if initialized was set to null in triggerUpdate. That'd mean
|
||||
// that during initilazation changes where triggered which need
|
||||
// to be applied OR a metaInfo watcher was triggered before the
|
||||
// current hook was called
|
||||
// (during initialization all changes are blocked)
|
||||
if (tags === false && $root[rootConfigKey].initialized === null) {
|
||||
this.$nextTick(() => triggerUpdate(options, $root, 'init'))
|
||||
}
|
||||
|
||||
$root[rootConfigKey].initialized = true
|
||||
delete $root[rootConfigKey].initializing
|
||||
|
||||
// add the navigation guards if they havent been added yet
|
||||
// they are needed for the afterNavigation callback
|
||||
if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) {
|
||||
addNavGuards($root)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// add the navigation guards if requested
|
||||
if (options.refreshOnceOnNavigation) {
|
||||
addNavGuards($root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.$on('hook:destroyed', function () {
|
||||
// do not trigger refresh:
|
||||
// - when user configured to not wait for transitions on destroyed
|
||||
// - when the component doesnt have a parent
|
||||
// - doesnt have metaInfo defined
|
||||
if (!this.$parent || !hasMetaInfo(this)) {
|
||||
return
|
||||
}
|
||||
delete this._hasMetaInfo
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (!options.waitOnDestroyed || !this.$el || !this.$el.offsetParent) {
|
||||
triggerUpdate(options, this.$root, 'destroyed')
|
||||
return
|
||||
}
|
||||
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) {
|
||||
/* istanbul ignore next line */
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(interval)
|
||||
|
||||
triggerUpdate(options, this.$root, 'destroyed')
|
||||
}, 50)
|
||||
})
|
||||
})
|
||||
|
||||
// do not trigger refresh on the server side
|
||||
if (this.$isServer) {
|
||||
/* istanbul ignore next */
|
||||
return
|
||||
}
|
||||
|
||||
// no need to add this hooks on server side
|
||||
updateOnLifecycleHook.forEach((lifecycleHook) => {
|
||||
this.$on(`hook:${lifecycleHook}`, function () {
|
||||
triggerUpdate(options, this[rootKey], lifecycleHook)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { isFunction } from '../utils/is-type'
|
||||
import { rootConfigKey } from './constants'
|
||||
import { pause, resume } from './pausing'
|
||||
|
||||
export function addNavGuards (rootVm) {
|
||||
const router = rootVm.$router
|
||||
|
||||
// return when nav guards already added or no router exists
|
||||
if (rootVm[rootConfigKey].navGuards || !router) {
|
||||
/* istanbul ignore next */
|
||||
return
|
||||
}
|
||||
|
||||
rootVm[rootConfigKey].navGuards = true
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
pause(rootVm)
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
rootVm.$nextTick(() => {
|
||||
const { metaInfo } = resume(rootVm)
|
||||
|
||||
if (metaInfo && isFunction(metaInfo.afterNavigation)) {
|
||||
metaInfo.afterNavigation(metaInfo)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { isObject, isUndefined } from '../utils/is-type'
|
||||
import { defaultOptions } from './constants'
|
||||
|
||||
export function setOptions (options) {
|
||||
// combine options
|
||||
options = isObject(options) ? options : {}
|
||||
|
||||
// The options are set like this so they can
|
||||
// be minified by terser while keeping the
|
||||
// user api intact
|
||||
// terser --mangle-properties keep_quoted=strict
|
||||
/* eslint-disable dot-notation */
|
||||
return {
|
||||
keyName: options['keyName'] || defaultOptions.keyName,
|
||||
attribute: options['attribute'] || defaultOptions.attribute,
|
||||
ssrAttribute: options['ssrAttribute'] || defaultOptions.ssrAttribute,
|
||||
tagIDKeyName: options['tagIDKeyName'] || defaultOptions.tagIDKeyName,
|
||||
contentKeyName: options['contentKeyName'] || defaultOptions.contentKeyName,
|
||||
metaTemplateKeyName: options['metaTemplateKeyName'] || defaultOptions.metaTemplateKeyName,
|
||||
debounceWait: isUndefined(options['debounceWait']) ? defaultOptions.debounceWait : options['debounceWait'],
|
||||
waitOnDestroyed: isUndefined(options['waitOnDestroyed']) ? defaultOptions.waitOnDestroyed : options['waitOnDestroyed'],
|
||||
ssrAppId: options['ssrAppId'] || defaultOptions.ssrAppId,
|
||||
refreshOnceOnNavigation: !!options['refreshOnceOnNavigation']
|
||||
}
|
||||
/* eslint-enable dot-notation */
|
||||
}
|
||||
|
||||
export function getOptions (options) {
|
||||
const optionsCopy = {}
|
||||
for (const key in options) {
|
||||
optionsCopy[key] = options[key]
|
||||
}
|
||||
return optionsCopy
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { rootConfigKey } from './constants'
|
||||
|
||||
export function pause (rootVm, refresh) {
|
||||
rootVm[rootConfigKey].pausing = true
|
||||
|
||||
return () => resume(rootVm, refresh)
|
||||
}
|
||||
|
||||
export function resume (rootVm, refresh) {
|
||||
rootVm[rootConfigKey].pausing = false
|
||||
|
||||
if (refresh || refresh === undefined) {
|
||||
return rootVm.$meta().refresh()
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { isUndefined, isFunction } from '../utils/is-type'
|
||||
|
||||
export function applyTemplate ({ component, metaTemplateKeyName, contentKeyName }, headObject, template, chunk) {
|
||||
if (template === true || headObject[metaTemplateKeyName] === true) {
|
||||
// abort, template was already applied
|
||||
return false
|
||||
}
|
||||
|
||||
if (isUndefined(template) && headObject[metaTemplateKeyName]) {
|
||||
template = headObject[metaTemplateKeyName]
|
||||
headObject[metaTemplateKeyName] = true
|
||||
}
|
||||
|
||||
// return early if no template defined
|
||||
if (!template) {
|
||||
// cleanup faulty template properties
|
||||
delete headObject[metaTemplateKeyName]
|
||||
return false
|
||||
}
|
||||
|
||||
if (isUndefined(chunk)) {
|
||||
chunk = headObject[contentKeyName]
|
||||
}
|
||||
|
||||
headObject[contentKeyName] = isFunction(template)
|
||||
? template.call(component, chunk)
|
||||
: template.replace(/%s/g, chunk)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
import { MetainfoActive } from './types'
|
||||
|
||||
export const hasSymbol =
|
||||
typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'
|
||||
|
||||
export const PolySymbol = (name: string) =>
|
||||
// vm = vue meta
|
||||
hasSymbol
|
||||
? Symbol(__DEV__ ? '[vue-meta]: ' + name : name)
|
||||
: (__DEV__ ? '[vue-meta]: ' : '_vm_') + name
|
||||
|
||||
export const metaInfoKey = PolySymbol(
|
||||
__DEV__ ? 'metainfo' : 'mi'
|
||||
) as InjectionKey<MetainfoActive>
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ComponentInternalInstance } from 'vue'
|
||||
import { Manager } from '../manager'
|
||||
|
||||
export type Immutable<T> = {
|
||||
readonly [P in keyof T]: Immutable<T[P]>
|
||||
}
|
||||
|
||||
export type TODO = any
|
||||
export type PathSegments = Array<string>
|
||||
|
||||
export interface MetainfoInput {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export interface MetainfoActive {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export type MetaContext = {
|
||||
id: string | symbol
|
||||
vm?: ComponentInternalInstance | null
|
||||
manager: Manager
|
||||
}
|
||||
|
||||
export type ActiveResolverSetup = (context: MetaContext) => void
|
||||
export type ActiveResolverMethod = (
|
||||
key: string,
|
||||
pathSegments: PathSegments,
|
||||
shadow: ShadowNode,
|
||||
active: ActiveNode
|
||||
) => any
|
||||
|
||||
export interface ActiveResolverObject {
|
||||
setup?: ActiveResolverSetup
|
||||
resolve: ActiveResolverMethod
|
||||
}
|
||||
|
||||
export interface ShadowNode {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export interface ActiveNode {
|
||||
[key: string]: TODO
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { inject, getCurrentInstance, onUnmounted } from 'vue'
|
||||
import { setByObject, remove } from './info'
|
||||
import { Manager } from './manager'
|
||||
import { createProxy, createHandler } from './proxy'
|
||||
import { metaInfoKey, PolySymbol } from './symbols'
|
||||
import { MetaContext, MetainfoActive, MetainfoInput } from './types'
|
||||
|
||||
let contextCounter: number = 0
|
||||
|
||||
export function useMeta(obj: MetainfoInput, manager?: Manager) {
|
||||
const vm = getCurrentInstance()
|
||||
if (vm) {
|
||||
console.log(vm)
|
||||
manager = vm.appContext.config.globalProperties.$metaManager
|
||||
}
|
||||
|
||||
if (!manager) {
|
||||
// oopsydoopsy
|
||||
throw new Error('No manager or current instance')
|
||||
return
|
||||
}
|
||||
|
||||
const context: MetaContext = {
|
||||
id: PolySymbol(`context ${contextCounter++}`),
|
||||
vm,
|
||||
manager,
|
||||
}
|
||||
|
||||
let unmount = <T extends Function = () => any>() => remove(context)
|
||||
if (vm) {
|
||||
onUnmounted(unmount)
|
||||
}
|
||||
|
||||
if (manager.resolver.setup) {
|
||||
manager.resolver.setup(context)
|
||||
}
|
||||
|
||||
setByObject(context, obj)
|
||||
|
||||
const handler = /*#__PURE__*/ createHandler(context)
|
||||
const meta = createProxy(obj, handler)
|
||||
|
||||
return {
|
||||
meta,
|
||||
unmount,
|
||||
}
|
||||
}
|
||||
|
||||
export function useMetainfo(): MetainfoActive {
|
||||
return inject(metaInfoKey)!
|
||||
}
|
||||
|
||||
export function getCurrentManager(): Manager {
|
||||
const vm = getCurrentInstance()!
|
||||
return vm.appContext.config.globalProperties.$metaManager
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* To reduce build size, this file provides simple polyfills without
|
||||
* overly excessive type checking and without modifying
|
||||
* the global Array.prototype
|
||||
* The polyfills are automatically removed in the commonjs build
|
||||
* Also, only files in client/ & shared/ should use these functions
|
||||
* files in server/ still use normal js function
|
||||
*/
|
||||
|
||||
// this const is replaced by rollup to true for umd builds
|
||||
// which means the polyfills are removed for other build formats
|
||||
const polyfill = process.env.NODE_ENV === 'test'
|
||||
|
||||
export function find (array, predicate, thisArg) {
|
||||
if (polyfill && !Array.prototype.find) {
|
||||
// idx needs to be a Number, for..in returns string
|
||||
for (let idx = 0; idx < array.length; idx++) {
|
||||
if (predicate.call(thisArg, array[idx], idx, array)) {
|
||||
return array[idx]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
return array.find(predicate, thisArg)
|
||||
}
|
||||
|
||||
export function findIndex (array, predicate, thisArg) {
|
||||
if (polyfill && !Array.prototype.findIndex) {
|
||||
// idx needs to be a Number, for..in returns string
|
||||
for (let idx = 0; idx < array.length; idx++) {
|
||||
if (predicate.call(thisArg, array[idx], idx, array)) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return array.findIndex(predicate, thisArg)
|
||||
}
|
||||
|
||||
export function toArray (arg) {
|
||||
if (polyfill && !Array.from) {
|
||||
return Array.prototype.slice.call(arg)
|
||||
}
|
||||
return Array.from(arg)
|
||||
}
|
||||
|
||||
export function includes (array, value) {
|
||||
if (polyfill && !Array.prototype.includes) {
|
||||
for (const idx in array) {
|
||||
if (array[idx] === value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return array.includes(value)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isArray, isObject } from '@vue/shared'
|
||||
// See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
|
||||
|
||||
export function clone(v: any): any {
|
||||
if (isArray(v)) {
|
||||
return v.map(clone)
|
||||
}
|
||||
|
||||
if (isObject(v)) {
|
||||
const res: any = {}
|
||||
|
||||
for (const key in v) {
|
||||
if (key === 'context') {
|
||||
res[key] = v[key]
|
||||
} else {
|
||||
res[key] = clone(v[key])
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { toArray } from './array'
|
||||
|
||||
export const querySelector = (arg, el) => (el || document).querySelectorAll(arg)
|
||||
|
||||
export function getTag (tags, tag) {
|
||||
if (!tags[tag]) {
|
||||
tags[tag] = document.getElementsByTagName(tag)[0]
|
||||
}
|
||||
|
||||
return tags[tag]
|
||||
}
|
||||
|
||||
export function getElementsKey ({ body, pbody }) {
|
||||
return body
|
||||
? 'body'
|
||||
: (pbody ? 'pbody' : 'head')
|
||||
}
|
||||
|
||||
export function queryElements (parentNode, { appId, attribute, type, tagIDKeyName }, attributes) {
|
||||
attributes = attributes || {}
|
||||
|
||||
const queries = [
|
||||
`${type}[${attribute}="${appId}"]`,
|
||||
`${type}[data-${tagIDKeyName}]`
|
||||
].map((query) => {
|
||||
for (const key in attributes) {
|
||||
const val = attributes[key]
|
||||
const attributeValue = val && val !== true ? `="${val}"` : ''
|
||||
query += `[data-${key}${attributeValue}]`
|
||||
}
|
||||
return query
|
||||
})
|
||||
|
||||
return toArray(querySelector(queries.join(', '), parentNode))
|
||||
}
|
||||
|
||||
export function removeElementsByAppId ({ attribute }, appId) {
|
||||
toArray(querySelector(`[${attribute}="${appId}"]`)).map(el => el.remove())
|
||||
}
|
||||
|
||||
export function removeAttribute (el, attributeName) {
|
||||
el.removeAttribute(attributeName)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { isArray, isObject } from './is-type'
|
||||
|
||||
export function ensureIsArray (arg, key) {
|
||||
if (!key || !isObject(arg)) {
|
||||
return isArray(arg) ? arg : []
|
||||
}
|
||||
|
||||
if (!isArray(arg[key])) {
|
||||
arg[key] = []
|
||||
}
|
||||
return arg
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './clone'
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* checks if passed argument is an array
|
||||
* @param {any} arg - the object to check
|
||||
* @return {Boolean} - true if `arg` is an array
|
||||
*/
|
||||
export function isArray (arg) {
|
||||
return Array.isArray(arg)
|
||||
}
|
||||
|
||||
export function isUndefined (arg) {
|
||||
return typeof arg === 'undefined'
|
||||
}
|
||||
|
||||
export function isObject (arg) {
|
||||
return typeof arg === 'object'
|
||||
}
|
||||
|
||||
export function isPureObject (arg) {
|
||||
return typeof arg === 'object' && arg !== null
|
||||
}
|
||||
|
||||
export function isFunction (arg) {
|
||||
return typeof arg === 'function'
|
||||
}
|
||||
|
||||
export function isString (arg) {
|
||||
return typeof arg === 'string'
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { isUndefined } from './is-type'
|
||||
|
||||
export function hasGlobalWindowFn () {
|
||||
try {
|
||||
return !isUndefined(window)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const hasGlobalWindow = hasGlobalWindowFn()
|
||||
+57
-44
@@ -15,7 +15,10 @@ describe(browserString, () => {
|
||||
const folder = path.resolve(__dirname, '..', 'fixtures/basic/.vue-meta/')
|
||||
|
||||
beforeAll(async () => {
|
||||
if (browserString.includes('browserstack') && browserString.includes('local')) {
|
||||
if (
|
||||
browserString.includes('browserstack') &&
|
||||
browserString.includes('local')
|
||||
) {
|
||||
const envFile = path.resolve(__dirname, '..', '..', '.env-browserstack')
|
||||
if (fs.existsSync(envFile)) {
|
||||
env(envFile)
|
||||
@@ -24,48 +27,52 @@ describe(browserString, () => {
|
||||
|
||||
const port = await getPort()
|
||||
|
||||
browser = await createBrowser(browserString, {
|
||||
folder,
|
||||
staticServer: {
|
||||
browser = await createBrowser(
|
||||
browserString,
|
||||
{
|
||||
folder,
|
||||
port
|
||||
},
|
||||
/* BrowserStackLocal: {
|
||||
staticServer: {
|
||||
folder,
|
||||
port,
|
||||
},
|
||||
/* BrowserStackLocal: {
|
||||
localIdentifier: Math.round(99999 * Math.random())
|
||||
}, */
|
||||
extendPage (page) {
|
||||
return {
|
||||
async navigate (path) {
|
||||
await page.runAsyncScript((path) => {
|
||||
return new Promise((resolve) => {
|
||||
const oldTitle = document.title
|
||||
extendPage(page) {
|
||||
return {
|
||||
async navigate(path) {
|
||||
await page.runAsyncScript(path => {
|
||||
return new Promise(resolve => {
|
||||
const oldTitle = document.title
|
||||
|
||||
// local firefox has sometimes not updated the title
|
||||
// even when the DOM is supposed to be fully updated
|
||||
const waitTitleChanged = function () {
|
||||
setTimeout(function () {
|
||||
if (oldTitle !== document.title) {
|
||||
resolve()
|
||||
} else {
|
||||
waitTitleChanged()
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
// local firefox has sometimes not updated the title
|
||||
// even when the DOM is supposed to be fully updated
|
||||
const waitTitleChanged = function () {
|
||||
setTimeout(function () {
|
||||
if (oldTitle !== document.title) {
|
||||
resolve()
|
||||
} else {
|
||||
waitTitleChanged()
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
window.$vueMeta.$once('routeChanged', waitTitleChanged)
|
||||
window.$vueMeta.$router.push(path)
|
||||
})
|
||||
}, path)
|
||||
},
|
||||
routeData () {
|
||||
return page.runScript(() => ({
|
||||
path: window.$vueMeta.$route.path,
|
||||
query: window.$vueMeta.$route.query
|
||||
}))
|
||||
window.$vueMeta.$once('routeChanged', waitTitleChanged)
|
||||
window.$vueMeta.$router.push(path)
|
||||
})
|
||||
}, path)
|
||||
},
|
||||
routeData() {
|
||||
return page.runScript(() => ({
|
||||
path: window.$vueMeta.$route.path,
|
||||
query: window.$vueMeta.$route.query,
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}, false)
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
browser.addCapability('browserstack.console', 'info')
|
||||
browser.addCapability('browserstack.networkLogs', 'true')
|
||||
@@ -86,7 +93,9 @@ describe(browserString, () => {
|
||||
|
||||
page = await browser.page(url)
|
||||
|
||||
expect(await page.getAttribute('html', 'data-vue-meta-server-rendered')).toBe(null)
|
||||
expect(
|
||||
await page.getAttribute('html', 'data-vue-meta-server-rendered')
|
||||
).toBe(null)
|
||||
expect(await page.getAttribute('html', 'lang')).toBe('en')
|
||||
expect(await page.getAttribute('html', 'amp')).toBe('')
|
||||
expect(await page.getAttribute('html', 'allowfullscreen')).toBe(null)
|
||||
@@ -110,13 +119,17 @@ describe(browserString, () => {
|
||||
expect(await page.getElementCount('body noscript:first-child')).toBe(1)
|
||||
expect(await page.getElementCount('body noscript:last-child')).toBe(1)
|
||||
|
||||
expect(await page.runScript(() => {
|
||||
return window.loadTest
|
||||
})).toBe('loaded')
|
||||
expect(
|
||||
await page.runScript(() => {
|
||||
return window.loadTest
|
||||
})
|
||||
).toBe('loaded')
|
||||
|
||||
expect(await page.runScript(() => {
|
||||
return window.loadCallback
|
||||
})).toBe('yes')
|
||||
expect(
|
||||
await page.runScript(() => {
|
||||
return window.loadCallback
|
||||
})
|
||||
).toBe('yes')
|
||||
})
|
||||
|
||||
test('/about', async () => {
|
||||
|
||||
@@ -18,7 +18,9 @@ describe('basic browser with ssr page', () => {
|
||||
expect(htmlTag).toContain(' lang="en" ')
|
||||
expect(htmlTag).toContain(' amp ')
|
||||
expect(htmlTag).not.toContain('allowfullscreen')
|
||||
expect(html.match(/<title[^>]*>(.*?)<\/title>/)[1]).toBe('Home | Vue Meta Test')
|
||||
expect(html.match(/<title[^>]*>(.*?)<\/title>/)[1]).toBe(
|
||||
'Home | Vue Meta Test'
|
||||
)
|
||||
expect(html.match(/<meta/g).length).toBe(2)
|
||||
expect(html.match(/<meta/g).length).toBe(2)
|
||||
|
||||
|
||||
Vendored
+3
-3
@@ -6,13 +6,13 @@ Vue.use(Router)
|
||||
|
||||
const Post = () => import('./views/about.vue')
|
||||
|
||||
export default function createRouter () {
|
||||
export default function createRouter() {
|
||||
return new Router({
|
||||
mode: 'hash',
|
||||
base: '/',
|
||||
routes: [
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/about', component: Post }
|
||||
]
|
||||
{ path: '/about', component: Post },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -3,7 +3,7 @@ import { _import, getVueMetaPath } from '../../utils/build'
|
||||
import App from './App.vue'
|
||||
import createRouter from './router'
|
||||
|
||||
export default async function createServerApp () {
|
||||
export default async function createServerApp() {
|
||||
const VueMeta = await _import(getVueMetaPath())
|
||||
|
||||
Vue.use(VueMeta)
|
||||
|
||||
+134
-79
@@ -1,7 +1,13 @@
|
||||
import { getComponentMetaInfo } from '../../src/shared/getComponentOption'
|
||||
import _getMetaInfo from '../../src/shared/getMetaInfo'
|
||||
import { triggerUpdate, batchUpdate } from '../../src/client/update'
|
||||
import { mount, createWrapper, loadVueMetaPlugin, vmTick, clearClientAttributeMap } from '../utils'
|
||||
import {
|
||||
mount,
|
||||
createWrapper,
|
||||
loadVueMetaPlugin,
|
||||
vmTick,
|
||||
clearClientAttributeMap,
|
||||
} from '../utils'
|
||||
import { defaultOptions } from '../../src/shared/constants'
|
||||
|
||||
import GoodbyeWorld from '../components/goodbye-world.vue'
|
||||
@@ -9,11 +15,12 @@ import HelloWorld from '../components/hello-world.vue'
|
||||
import KeepAlive from '../components/keep-alive.vue'
|
||||
import Changed from '../components/changed.vue'
|
||||
|
||||
const getMetaInfo = component => _getMetaInfo(defaultOptions, getComponentMetaInfo(defaultOptions, component))
|
||||
const getMetaInfo = component =>
|
||||
_getMetaInfo(defaultOptions, getComponentMetaInfo(defaultOptions, component))
|
||||
|
||||
jest.mock('../../src/client/update')
|
||||
jest.mock('../../src/utils/window', () => ({
|
||||
hasGlobalWindow: false
|
||||
hasGlobalWindow: false,
|
||||
}))
|
||||
|
||||
describe('components', () => {
|
||||
@@ -30,21 +37,21 @@ describe('components', () => {
|
||||
elements = {
|
||||
html: document.createElement('html'),
|
||||
head: document.createElement('head'),
|
||||
body: document.createElement('body')
|
||||
body: document.createElement('body'),
|
||||
}
|
||||
|
||||
elements.html.appendChild(elements.head)
|
||||
elements.html.appendChild(elements.body)
|
||||
|
||||
document._getElementsByTagName = document.getElementsByTagName
|
||||
jest.spyOn(document, 'getElementsByTagName').mockImplementation((tag) => {
|
||||
jest.spyOn(document, 'getElementsByTagName').mockImplementation(tag => {
|
||||
if (elements[tag]) {
|
||||
return [elements[tag]]
|
||||
}
|
||||
|
||||
return document._getElementsByTagName(tag)
|
||||
})
|
||||
jest.spyOn(document, 'querySelectorAll').mockImplementation((query) => {
|
||||
jest.spyOn(document, 'querySelectorAll').mockImplementation(query => {
|
||||
return elements.html.querySelectorAll(query)
|
||||
})
|
||||
})
|
||||
@@ -52,16 +59,22 @@ describe('components', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
elements.html.getAttributeNames().forEach(name => elements.html.removeAttribute(name))
|
||||
elements.html
|
||||
.getAttributeNames()
|
||||
.forEach(name => elements.html.removeAttribute(name))
|
||||
elements.head.childNodes.forEach(child => child.remove())
|
||||
elements.head.getAttributeNames().forEach(name => elements.head.removeAttribute(name))
|
||||
elements.head
|
||||
.getAttributeNames()
|
||||
.forEach(name => elements.head.removeAttribute(name))
|
||||
elements.body.childNodes.forEach(child => child.remove())
|
||||
elements.body.getAttributeNames().forEach(name => elements.body.removeAttribute(name))
|
||||
elements.body
|
||||
.getAttributeNames()
|
||||
.forEach(name => elements.body.removeAttribute(name))
|
||||
|
||||
clearClientAttributeMap()
|
||||
})
|
||||
|
||||
test('meta-info refreshed on component\'s data change', () => {
|
||||
test("meta-info refreshed on component's data change", () => {
|
||||
const wrapper = mount(HelloWorld, { localVue: Vue })
|
||||
|
||||
let metaInfo = getMetaInfo(wrapper.vm)
|
||||
@@ -134,7 +147,9 @@ describe('components', () => {
|
||||
HelloWorld.metaInfo = metaInfo
|
||||
|
||||
expect(warn).toHaveBeenCalledTimes(1)
|
||||
expect(warn).toHaveBeenCalledWith('This vue app/component has no vue-meta configuration')
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'This vue app/component has no vue-meta configuration'
|
||||
)
|
||||
|
||||
warn.mockRestore()
|
||||
})
|
||||
@@ -151,7 +166,7 @@ describe('components', () => {
|
||||
const { set } = this.$meta().addApp('inject-test-app')
|
||||
set({
|
||||
htmlAttrs: { lang: 'nl' },
|
||||
meta: [{ name: 'description', content: 'test-description' }]
|
||||
meta: [{ name: 'description', content: 'test-description' }],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -160,8 +175,12 @@ describe('components', () => {
|
||||
const metaInfo = wrapper.vm.$meta().inject()
|
||||
expect(metaInfo.title.text()).toEqual('<title>Hello World</title>')
|
||||
|
||||
expect(metaInfo.htmlAttrs.text()).toEqual('lang="en nl" data-vue-meta="%7B%22lang%22:%7B%22ssr%22:%22en%22,%22inject-test-app%22:%22nl%22%7D%7D"')
|
||||
expect(metaInfo.meta.text()).toEqual('<meta data-vue-meta="ssr" charset="utf-8"><meta data-vue-meta="inject-test-app" name="description" content="test-description">')
|
||||
expect(metaInfo.htmlAttrs.text()).toEqual(
|
||||
'lang="en nl" data-vue-meta="%7B%22lang%22:%7B%22ssr%22:%22en%22,%22inject-test-app%22:%22nl%22%7D%7D"'
|
||||
)
|
||||
expect(metaInfo.meta.text()).toEqual(
|
||||
'<meta data-vue-meta="ssr" charset="utf-8"><meta data-vue-meta="inject-test-app" name="description" content="test-description">'
|
||||
)
|
||||
|
||||
delete HelloWorld.created
|
||||
})
|
||||
@@ -170,15 +189,19 @@ describe('components', () => {
|
||||
HelloWorld.created = function () {
|
||||
const { set } = this.$meta().addApp('inject-test-app')
|
||||
set({
|
||||
meta: [{ skip: true, name: 'description', content: 'test-description' }],
|
||||
script: [{
|
||||
once: true,
|
||||
callback: true,
|
||||
async: false,
|
||||
json: {
|
||||
a: 1
|
||||
}
|
||||
}]
|
||||
meta: [
|
||||
{ skip: true, name: 'description', content: 'test-description' },
|
||||
],
|
||||
script: [
|
||||
{
|
||||
once: true,
|
||||
callback: true,
|
||||
async: false,
|
||||
json: {
|
||||
a: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -186,8 +209,12 @@ describe('components', () => {
|
||||
|
||||
const metaInfo = wrapper.vm.$meta().inject()
|
||||
|
||||
expect(metaInfo.meta.text()).toEqual('<meta data-vue-meta="ssr" charset="utf-8">')
|
||||
expect(metaInfo.script.text()).toEqual('<script onload="this.__vm_l=1">{"a":1}</script>')
|
||||
expect(metaInfo.meta.text()).toEqual(
|
||||
'<meta data-vue-meta="ssr" charset="utf-8">'
|
||||
)
|
||||
expect(metaInfo.script.text()).toEqual(
|
||||
'<script onload="this.__vm_l=1">{"a":1}</script>'
|
||||
)
|
||||
|
||||
delete HelloWorld.created
|
||||
})
|
||||
@@ -202,9 +229,9 @@ describe('components', () => {
|
||||
|
||||
const Component = Vue.extend({
|
||||
metaInfo: { title: 'Test' },
|
||||
render (h) {
|
||||
render(h) {
|
||||
return h('div', null, 'Test')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const vm = new Component().$mount(el)
|
||||
@@ -249,7 +276,7 @@ describe('components', () => {
|
||||
const afterNavigation = jest.fn()
|
||||
const component = Vue.component('nav-component', {
|
||||
render: h => h('div'),
|
||||
metaInfo: { afterNavigation }
|
||||
metaInfo: { afterNavigation },
|
||||
})
|
||||
|
||||
const guards = {}
|
||||
@@ -257,14 +284,14 @@ describe('components', () => {
|
||||
localVue: Vue,
|
||||
mocks: {
|
||||
$router: {
|
||||
beforeEach (fn) {
|
||||
beforeEach(fn) {
|
||||
guards.before = fn
|
||||
},
|
||||
afterEach (fn) {
|
||||
afterEach(fn) {
|
||||
guards.after = fn
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await vmTick(wrapper.vm)
|
||||
@@ -286,7 +313,7 @@ describe('components', () => {
|
||||
const afterNavigation = jest.fn()
|
||||
const component = Vue.component('nav-component', {
|
||||
render: h => h('div'),
|
||||
metaInfo: { afterNavigation }
|
||||
metaInfo: { afterNavigation },
|
||||
})
|
||||
|
||||
const guards = {}
|
||||
@@ -294,14 +321,14 @@ describe('components', () => {
|
||||
localVue: Vue,
|
||||
mocks: {
|
||||
$router: {
|
||||
beforeEach (fn) {
|
||||
beforeEach(fn) {
|
||||
guards.before = fn
|
||||
},
|
||||
afterEach (fn) {
|
||||
afterEach(fn) {
|
||||
guards.after = fn
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await vmTick(wrapper.vm)
|
||||
@@ -334,27 +361,27 @@ describe('components', () => {
|
||||
// this component uses a computed prop to simulate a non-synchronous
|
||||
// metaInfo update like you would have with a Vuex mutation
|
||||
const Component = Vue.extend({
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
htmlAttrs: {
|
||||
theme: this.theme
|
||||
}
|
||||
theme: this.theme,
|
||||
},
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
hiddenTheme: 'light'
|
||||
hiddenTheme: 'light',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
theme () {
|
||||
theme() {
|
||||
return this.hiddenTheme
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeMount () {
|
||||
beforeMount() {
|
||||
this.hiddenTheme = 'dark'
|
||||
},
|
||||
render: h => h('div')
|
||||
render: h => h('div'),
|
||||
})
|
||||
|
||||
const vm = new Component().$mount(el)
|
||||
@@ -385,27 +412,27 @@ describe('components', () => {
|
||||
document.body.appendChild(el)
|
||||
|
||||
const Component = Vue.extend({
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
hiddenTheme: 'light'
|
||||
hiddenTheme: 'light',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
theme () {
|
||||
theme() {
|
||||
return this.hiddenTheme
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.hiddenTheme = 'dark'
|
||||
},
|
||||
render: h => h('div'),
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
htmlAttrs: {
|
||||
theme: this.theme
|
||||
}
|
||||
theme: this.theme,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const vm = new Component().$mount(el)
|
||||
@@ -431,7 +458,7 @@ describe('components', () => {
|
||||
// are really removed
|
||||
const { set, remove } = this.$meta().addApp('my-bogus-app')
|
||||
set({
|
||||
meta: [{ name: 'og:description', content: 'test-description' }]
|
||||
meta: [{ name: 'og:description', content: 'test-description' }],
|
||||
})
|
||||
remove()
|
||||
|
||||
@@ -439,12 +466,12 @@ describe('components', () => {
|
||||
app.set({
|
||||
htmlAttrs: { lang: 'nl' },
|
||||
meta: [{ name: 'description', content: 'test-description' }],
|
||||
script: [{ innerHTML: 'var test = true;' }]
|
||||
script: [{ innerHTML: 'var test = true;' }],
|
||||
})
|
||||
}
|
||||
|
||||
const wrapper = mount(HelloWorld, {
|
||||
localVue: Vue
|
||||
localVue: Vue,
|
||||
})
|
||||
|
||||
wrapper.vm.$meta().refresh()
|
||||
@@ -452,21 +479,28 @@ describe('components', () => {
|
||||
expect(html.getAttribute('lang')).toEqual('en nl')
|
||||
expect(Array.from(html.querySelectorAll('meta')).length).toBe(2)
|
||||
expect(Array.from(html.querySelectorAll('script')).length).toBe(1)
|
||||
expect(Array.from(html.querySelectorAll('[data-vue-meta="my-test-app"]')).length).toBe(2)
|
||||
expect(
|
||||
Array.from(html.querySelectorAll('[data-vue-meta="my-test-app"]')).length
|
||||
).toBe(2)
|
||||
|
||||
app.remove()
|
||||
|
||||
// add another app to make sure on client data is immediately added
|
||||
const anotherApp = wrapper.vm.$meta().addApp('another-test-app')
|
||||
anotherApp.set({
|
||||
meta: [{ name: 'og:description', content: 'test-description' }]
|
||||
meta: [{ name: 'og:description', content: 'test-description' }],
|
||||
})
|
||||
|
||||
expect(html.getAttribute('lang')).toEqual('en')
|
||||
expect(Array.from(html.querySelectorAll('meta')).length).toBe(2)
|
||||
expect(Array.from(html.querySelectorAll('script')).length).toBe(0)
|
||||
expect(Array.from(html.querySelectorAll('[data-vue-meta="my-test-app"]')).length).toBe(0)
|
||||
expect(Array.from(html.querySelectorAll('[data-vue-meta="another-test-app"]')).length).toBe(1)
|
||||
expect(
|
||||
Array.from(html.querySelectorAll('[data-vue-meta="my-test-app"]')).length
|
||||
).toBe(0)
|
||||
expect(
|
||||
Array.from(html.querySelectorAll('[data-vue-meta="another-test-app"]'))
|
||||
.length
|
||||
).toBe(1)
|
||||
|
||||
wrapper.destroy()
|
||||
delete HelloWorld.created
|
||||
@@ -477,7 +511,10 @@ describe('components', () => {
|
||||
html.setAttribute(defaultOptions.ssrAttribute, 'true')
|
||||
|
||||
body.setAttribute('foo', 'bar')
|
||||
body.setAttribute('data-vue-meta', '%7B%22foo%22:%7B%22ssr%22:%22bar%22%7D%7D')
|
||||
body.setAttribute(
|
||||
'data-vue-meta',
|
||||
'%7B%22foo%22:%7B%22ssr%22:%22bar%22%7D%7D'
|
||||
)
|
||||
|
||||
const el = document.createElement('div')
|
||||
el.setAttribute('id', 'app')
|
||||
@@ -488,10 +525,10 @@ describe('components', () => {
|
||||
metaInfo: {
|
||||
title: 'Test',
|
||||
bodyAttrs: {
|
||||
foo: 'bar'
|
||||
}
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
render: h => h('div', null, 'Test')
|
||||
render: h => h('div', null, 'Test'),
|
||||
})
|
||||
|
||||
const vm = new Component().$mount(el)
|
||||
@@ -500,7 +537,9 @@ describe('components', () => {
|
||||
|
||||
wrapper.vm.$meta().refresh()
|
||||
expect(body.getAttribute('foo')).toBe('bar')
|
||||
expect(body.getAttribute('data-vue-meta')).toBe('%7B%22foo%22:%7B%22ssr%22:%22bar%22%7D%7D')
|
||||
expect(body.getAttribute('data-vue-meta')).toBe(
|
||||
'%7B%22foo%22:%7B%22ssr%22:%22bar%22%7D%7D'
|
||||
)
|
||||
|
||||
wrapper.vm.$meta().refresh()
|
||||
expect(body.getAttribute('foo')).toBe('bar')
|
||||
@@ -519,14 +558,14 @@ describe('components', () => {
|
||||
localVue: Vue,
|
||||
mocks: {
|
||||
$router: {
|
||||
beforeEach (fn) {
|
||||
beforeEach(fn) {
|
||||
guards.before = fn
|
||||
},
|
||||
afterEach (fn) {
|
||||
afterEach(fn) {
|
||||
guards.after = fn
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(guards.before).toBeUndefined()
|
||||
@@ -540,8 +579,13 @@ describe('components', () => {
|
||||
|
||||
test('destroyed hook calls triggerUpdate delayed', async () => {
|
||||
jest.useFakeTimers()
|
||||
const wrapper = mount(HelloWorld, { localVue: Vue, parentComponent: { render: h => h('div') } })
|
||||
const spy = jest.spyOn(wrapper.vm.$el, 'offsetParent', 'get').mockReturnValue(true)
|
||||
const wrapper = mount(HelloWorld, {
|
||||
localVue: Vue,
|
||||
parentComponent: { render: h => h('div') },
|
||||
})
|
||||
const spy = jest
|
||||
.spyOn(wrapper.vm.$el, 'offsetParent', 'get')
|
||||
.mockReturnValue(true)
|
||||
|
||||
wrapper.destroy()
|
||||
|
||||
@@ -553,19 +597,30 @@ describe('components', () => {
|
||||
jest.advanceTimersByTime(51)
|
||||
|
||||
expect(triggerUpdate).toHaveBeenCalledTimes(2)
|
||||
expect(triggerUpdate).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'destroyed')
|
||||
expect(triggerUpdate).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
'destroyed'
|
||||
)
|
||||
})
|
||||
|
||||
test('destroyed hook calls triggerUpdate immediately when waitOnDestroyed: false', async () => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
const wrapper = mount(HelloWorld, { localVue: Vue, parentComponent: { render: h => h('div') } })
|
||||
const wrapper = mount(HelloWorld, {
|
||||
localVue: Vue,
|
||||
parentComponent: { render: h => h('div') },
|
||||
})
|
||||
wrapper.vm.$meta().setOptions({ waitOnDestroyed: false })
|
||||
wrapper.destroy()
|
||||
|
||||
await vmTick(wrapper.vm)
|
||||
|
||||
expect(triggerUpdate).toHaveBeenCalledTimes(2)
|
||||
expect(triggerUpdate).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'destroyed')
|
||||
expect(triggerUpdate).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
'destroyed'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
+31
-24
@@ -4,7 +4,12 @@ import { loadVueMetaPlugin } from '../utils'
|
||||
import { defaultOptions } from '../../src/shared/constants'
|
||||
import { serverSequences } from '../../src/shared/escaping'
|
||||
|
||||
const getMetaInfo = (component, escapeSequences) => _getMetaInfo(defaultOptions, getComponentMetaInfo(defaultOptions, component), escapeSequences)
|
||||
const getMetaInfo = (component, escapeSequences) =>
|
||||
_getMetaInfo(
|
||||
defaultOptions,
|
||||
getComponentMetaInfo(defaultOptions, component),
|
||||
escapeSequences
|
||||
)
|
||||
|
||||
describe('escaping', () => {
|
||||
let Vue
|
||||
@@ -17,8 +22,8 @@ describe('escaping', () => {
|
||||
htmlAttrs: { key: 1 },
|
||||
title: 'Hello & Goodbye',
|
||||
script: [{ innerHTML: 'Hello & Goodbye' }],
|
||||
__dangerouslyDisableSanitizers: ['script']
|
||||
}
|
||||
__dangerouslyDisableSanitizers: ['script'],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
||||
@@ -26,7 +31,7 @@ describe('escaping', () => {
|
||||
titleChunk: 'Hello & Goodbye',
|
||||
titleTemplate: '%s',
|
||||
htmlAttrs: {
|
||||
key: 1
|
||||
key: 1,
|
||||
},
|
||||
headAttrs: {},
|
||||
bodyAttrs: {},
|
||||
@@ -37,15 +42,15 @@ describe('escaping', () => {
|
||||
script: [{ innerHTML: 'Hello & Goodbye' }],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: ['script'],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
test('null title is left as it is', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: null
|
||||
}
|
||||
title: null,
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
||||
@@ -62,7 +67,7 @@ describe('escaping', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,10 +77,10 @@ describe('escaping', () => {
|
||||
title: 'Hello',
|
||||
script: [
|
||||
{ vmid: 'yescape', innerHTML: 'Hello & Goodbye' },
|
||||
{ vmid: 'noscape', innerHTML: 'Hello & Goodbye' }
|
||||
{ vmid: 'noscape', innerHTML: 'Hello & Goodbye' },
|
||||
],
|
||||
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] }
|
||||
}
|
||||
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] },
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
||||
@@ -91,11 +96,11 @@ describe('escaping', () => {
|
||||
style: [],
|
||||
script: [
|
||||
{ innerHTML: 'Hello & Goodbye', vmid: 'yescape' },
|
||||
{ innerHTML: 'Hello & Goodbye', vmid: 'noscape' }
|
||||
{ innerHTML: 'Hello & Goodbye', vmid: 'noscape' },
|
||||
],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] }
|
||||
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -105,12 +110,13 @@ describe('escaping', () => {
|
||||
script: [
|
||||
{
|
||||
json: {
|
||||
perfectlySave: '</script><p class="unsafe">This is safe</p><script>',
|
||||
'</script>unsafeKey': 'This is also still safe'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
perfectlySave:
|
||||
'</script><p class="unsafe">This is safe</p><script>',
|
||||
'</script>unsafeKey': 'This is also still safe',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component, serverSequences)).toEqual({
|
||||
@@ -127,14 +133,15 @@ describe('escaping', () => {
|
||||
script: [
|
||||
{
|
||||
json: {
|
||||
perfectlySave: '</script><p class="unsafe">This is safe</p><script>',
|
||||
'</script>unsafeKey': 'This is also still safe'
|
||||
}
|
||||
}
|
||||
perfectlySave:
|
||||
'</script><p class="unsafe">This is safe</p><script>',
|
||||
'</script>unsafeKey': 'This is also still safe',
|
||||
},
|
||||
},
|
||||
],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,14 +3,15 @@ import { defaultOptions } from '../../src/shared/constants'
|
||||
import metaInfoData from '../utils/meta-info-data'
|
||||
import { titleGenerator } from '../../src/server/generators'
|
||||
|
||||
const generateServerInjector = metaInfo => _generateServerInjector(defaultOptions, metaInfo).injectors
|
||||
const generateServerInjector = metaInfo =>
|
||||
_generateServerInjector(defaultOptions, metaInfo).injectors
|
||||
|
||||
describe('generators', () => {
|
||||
for (const type in metaInfoData) {
|
||||
const typeTests = metaInfoData[type]
|
||||
|
||||
const testCases = {
|
||||
add: (tags) => {
|
||||
add: tags => {
|
||||
let html = tags.text()
|
||||
|
||||
// ssr only returns the attributes, convert to full tag
|
||||
@@ -18,14 +19,14 @@ describe('generators', () => {
|
||||
html = `<${type.substr(0, 4)} ${html}>`
|
||||
}
|
||||
|
||||
typeTests.add.expect.forEach((expected) => {
|
||||
typeTests.add.expect.forEach(expected => {
|
||||
expect(html).toContain(expected)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
describe(`${type} type tests`, () => {
|
||||
Object.keys(typeTests).forEach((action) => {
|
||||
Object.keys(typeTests).forEach(action => {
|
||||
const testInfo = typeTests[action]
|
||||
|
||||
// return when no test case available
|
||||
@@ -98,7 +99,9 @@ describe('extra tests', () => {
|
||||
|
||||
expect(scriptTags.text()).toBe('')
|
||||
expect(scriptTags.text({ body: true })).toBe('')
|
||||
expect(scriptTags.text({ pbody: true })).toBe('<script data-vue-meta="ssr" src="/script.js" data-pbody="true"></script>')
|
||||
expect(scriptTags.text({ pbody: true })).toBe(
|
||||
'<script data-vue-meta="ssr" src="/script.js" data-pbody="true"></script>'
|
||||
)
|
||||
})
|
||||
|
||||
test('script append body', () => {
|
||||
@@ -106,7 +109,9 @@ describe('extra tests', () => {
|
||||
const { script: scriptTags } = generateServerInjector({ script: tags })
|
||||
|
||||
expect(scriptTags.text()).toBe('')
|
||||
expect(scriptTags.text({ body: true })).toBe('<script data-vue-meta="ssr" src="/script.js" data-body="true"></script>')
|
||||
expect(scriptTags.text({ body: true })).toBe(
|
||||
'<script data-vue-meta="ssr" src="/script.js" data-body="true"></script>'
|
||||
)
|
||||
expect(scriptTags.text({ pbody: true })).toBe('')
|
||||
})
|
||||
|
||||
@@ -115,11 +120,11 @@ describe('extra tests', () => {
|
||||
title: 'hello',
|
||||
htmlAttrs: { lang: 'en' },
|
||||
bodyAttrs: { class: 'base-class' },
|
||||
script: [{ src: '/script.js', body: true }]
|
||||
script: [{ src: '/script.js', body: true }],
|
||||
}
|
||||
const extraInfo = {
|
||||
bodyAttrs: { class: 'extra-class' },
|
||||
script: [{ src: '/script.js', pbody: true }]
|
||||
script: [{ src: '/script.js', pbody: true }],
|
||||
}
|
||||
|
||||
const serverInjector = _generateServerInjector(defaultOptions, baseInfo)
|
||||
@@ -128,14 +133,26 @@ describe('extra tests', () => {
|
||||
const meta = serverInjector.injectors
|
||||
|
||||
expect(meta.script.text()).toBe('')
|
||||
expect(meta.script.text({ body: true })).toBe('<script data-vue-meta="ssr" src="/script.js" data-body="true"></script>')
|
||||
expect(meta.script.text({ pbody: true })).toBe('<script data-vue-meta="test-app" src="/script.js" data-pbody="true"></script>')
|
||||
expect(meta.script.text({ body: true })).toBe(
|
||||
'<script data-vue-meta="ssr" src="/script.js" data-body="true"></script>'
|
||||
)
|
||||
expect(meta.script.text({ pbody: true })).toBe(
|
||||
'<script data-vue-meta="test-app" src="/script.js" data-pbody="true"></script>'
|
||||
)
|
||||
|
||||
expect(meta.head(true)).toBe('<title>hello</title>\n')
|
||||
expect(meta.bodyPrepend(true)).toBe('<script data-vue-meta="test-app" src="/script.js" data-pbody="true"></script>\n')
|
||||
expect(meta.bodyAppend()).toBe('<script data-vue-meta="ssr" src="/script.js" data-body="true"></script>')
|
||||
expect(meta.bodyPrepend(true)).toBe(
|
||||
'<script data-vue-meta="test-app" src="/script.js" data-pbody="true"></script>\n'
|
||||
)
|
||||
expect(meta.bodyAppend()).toBe(
|
||||
'<script data-vue-meta="ssr" src="/script.js" data-body="true"></script>'
|
||||
)
|
||||
|
||||
expect(meta.htmlAttrs.text()).toBe('lang="en" data-vue-meta="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D"')
|
||||
expect(meta.bodyAttrs.text()).toBe('class="base-class extra-class" data-vue-meta="%7B%22class%22:%7B%22ssr%22:%22base-class%22,%22test-app%22:%22extra-class%22%7D%7D"')
|
||||
expect(meta.htmlAttrs.text()).toBe(
|
||||
'lang="en" data-vue-meta="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D"'
|
||||
)
|
||||
expect(meta.bodyAttrs.text()).toBe(
|
||||
'class="base-class extra-class" data-vue-meta="%7B%22class%22:%7B%22ssr%22:%22base-class%22,%22test-app%22:%22extra-class%22%7D%7D"'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,10 @@ describe('getComponentOption', () => {
|
||||
|
||||
test('fetches the given option from the given component', () => {
|
||||
const component = new Vue({ someOption: { foo: 'bar' } })
|
||||
const mergedOption = getComponentOption({ keyName: 'someOption' }, component)
|
||||
const mergedOption = getComponentOption(
|
||||
{ keyName: 'someOption' },
|
||||
component
|
||||
)
|
||||
expect(mergedOption.foo).toBeDefined()
|
||||
expect(mergedOption.foo).toEqual('bar')
|
||||
})
|
||||
@@ -23,14 +26,14 @@ describe('getComponentOption', () => {
|
||||
test('calls a function as computed prop, injecting the component as context', () => {
|
||||
const component = new Vue({
|
||||
name: 'Foobar',
|
||||
someFunc () {
|
||||
someFunc() {
|
||||
return { opt: this.name }
|
||||
},
|
||||
computed: {
|
||||
$metaInfo () {
|
||||
$metaInfo() {
|
||||
return this.$options.someFunc()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const mergedOption = getComponentOption({ keyName: 'someFunc' }, component)
|
||||
@@ -41,11 +44,14 @@ describe('getComponentOption', () => {
|
||||
|
||||
test('fetches deeply nested component options and merges them', () => {
|
||||
const localVue = loadVueMetaPlugin({ keyName: 'foo' })
|
||||
localVue.component('merge-child', { render: h => h('div'), foo: { bar: 'baz' } })
|
||||
localVue.component('merge-child', {
|
||||
render: h => h('div'),
|
||||
foo: { bar: 'baz' },
|
||||
})
|
||||
|
||||
const component = localVue.component('parent', {
|
||||
foo: { fizz: 'buzz' },
|
||||
render: h => h('div', null, [h('merge-child')])
|
||||
render: h => h('div', null, [h('merge-child')]),
|
||||
})
|
||||
|
||||
const wrapper = mount(component, { localVue })
|
||||
@@ -96,23 +102,24 @@ describe('getComponentOption', () => {
|
||||
|
||||
localVue.component('meta-child', {
|
||||
foo: { bar: 'baz' },
|
||||
render (h) {
|
||||
render(h) {
|
||||
return h('div', this.$slots.default)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
localVue.component('nometa-child', {
|
||||
render (h) {
|
||||
render(h) {
|
||||
return h('div', this.$slots.default)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const component = localVue.component('parent', {
|
||||
render: h => h('div', null, [
|
||||
h('meta-child', null, [h('nometa-child')]),
|
||||
h('nometa-child', null, [h('meta-child')]),
|
||||
h('nometa-child')
|
||||
])
|
||||
render: h =>
|
||||
h('div', null, [
|
||||
h('meta-child', null, [h('nometa-child')]),
|
||||
h('nometa-child', null, [h('meta-child')]),
|
||||
h('nometa-child'),
|
||||
]),
|
||||
})
|
||||
|
||||
const wrapper = mount(component, { localVue })
|
||||
@@ -136,22 +143,20 @@ describe('getComponentOption', () => {
|
||||
|
||||
localVue.component('meta-child', {
|
||||
foo: { bar: 'baz' },
|
||||
render (h) {
|
||||
render(h) {
|
||||
return h('div', this.$slots.default)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
localVue.component('nometa-child', {
|
||||
render (h) {
|
||||
render(h) {
|
||||
return h('div', this.$slots.default)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const component = localVue.component('parent', {
|
||||
foo: () => {},
|
||||
render: h => h('div', null, [
|
||||
h('meta-child')
|
||||
])
|
||||
render: h => h('div', null, [h('meta-child')]),
|
||||
})
|
||||
|
||||
const wrapper = mount(component, { localVue })
|
||||
|
||||
+158
-168
@@ -3,7 +3,13 @@ import _getMetaInfo from '../../src/shared/getMetaInfo'
|
||||
import { loadVueMetaPlugin } from '../utils'
|
||||
import { defaultOptions } from '../../src/shared/constants'
|
||||
|
||||
const getMetaInfo = component => _getMetaInfo(defaultOptions, getComponentMetaInfo(defaultOptions, component), [], component)
|
||||
const getMetaInfo = component =>
|
||||
_getMetaInfo(
|
||||
defaultOptions,
|
||||
getComponentMetaInfo(defaultOptions, component),
|
||||
[],
|
||||
component
|
||||
)
|
||||
|
||||
describe('getMetaInfo', () => {
|
||||
let Vue
|
||||
@@ -27,7 +33,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,10 +41,8 @@ describe('getMetaInfo', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
]
|
||||
}
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -48,16 +52,14 @@ describe('getMetaInfo', () => {
|
||||
htmlAttrs: {},
|
||||
headAttrs: {},
|
||||
bodyAttrs: {},
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
],
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
base: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -65,8 +67,8 @@ describe('getMetaInfo', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
base: { href: 'href' }
|
||||
}
|
||||
base: { href: 'href' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -77,15 +79,13 @@ describe('getMetaInfo', () => {
|
||||
headAttrs: {},
|
||||
bodyAttrs: {},
|
||||
meta: [],
|
||||
base: [
|
||||
{ href: 'href' }
|
||||
],
|
||||
base: [{ href: 'href' }],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -97,15 +97,15 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'a',
|
||||
property: 'a',
|
||||
content: 'a'
|
||||
content: 'a',
|
||||
},
|
||||
{
|
||||
vmid: 'a',
|
||||
property: 'a',
|
||||
content: 'b'
|
||||
}
|
||||
]
|
||||
}
|
||||
content: 'b',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -119,8 +119,8 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'a',
|
||||
property: 'a',
|
||||
content: 'a'
|
||||
}
|
||||
content: 'a',
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -128,7 +128,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -137,10 +137,8 @@ describe('getMetaInfo', () => {
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
titleTemplate: '%s World',
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
]
|
||||
}
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -150,16 +148,14 @@ describe('getMetaInfo', () => {
|
||||
htmlAttrs: {},
|
||||
headAttrs: {},
|
||||
bodyAttrs: {},
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
],
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
base: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -170,10 +166,8 @@ describe('getMetaInfo', () => {
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
titleTemplate,
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
]
|
||||
}
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -183,16 +177,14 @@ describe('getMetaInfo', () => {
|
||||
htmlAttrs: {},
|
||||
headAttrs: {},
|
||||
bodyAttrs: {},
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
],
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
base: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -205,15 +197,13 @@ describe('getMetaInfo', () => {
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
titleTemplate,
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
]
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
helloWorldText: 'Function World'
|
||||
helloWorldText: 'Function World',
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -223,16 +213,14 @@ describe('getMetaInfo', () => {
|
||||
htmlAttrs: {},
|
||||
headAttrs: {},
|
||||
bodyAttrs: {},
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
],
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
base: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -245,10 +233,10 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: '%s - My page'
|
||||
}
|
||||
]
|
||||
}
|
||||
template: '%s - My page',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const expectedMetaInfo = {
|
||||
@@ -263,8 +251,8 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title - My page',
|
||||
template: true
|
||||
}
|
||||
template: true,
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -272,7 +260,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
}
|
||||
|
||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||
@@ -288,10 +276,10 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: chunk => `${chunk} - My page`
|
||||
}
|
||||
]
|
||||
}
|
||||
template: chunk => `${chunk} - My page`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const expectedMetaInfo = {
|
||||
@@ -306,8 +294,8 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title - My page',
|
||||
template: true
|
||||
}
|
||||
template: true,
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -315,7 +303,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
}
|
||||
|
||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||
@@ -330,10 +318,10 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title'
|
||||
}
|
||||
]
|
||||
}
|
||||
content: 'Test title',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -347,8 +335,8 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title'
|
||||
}
|
||||
content: 'Test title',
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -356,7 +344,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -369,10 +357,10 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: null
|
||||
}
|
||||
]
|
||||
}
|
||||
template: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -386,8 +374,8 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title'
|
||||
}
|
||||
content: 'Test title',
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -395,7 +383,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -408,10 +396,10 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: false
|
||||
}
|
||||
]
|
||||
}
|
||||
template: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -425,8 +413,8 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title'
|
||||
}
|
||||
content: 'Test title',
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -434,7 +422,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -447,10 +435,10 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'An important title!'
|
||||
}
|
||||
]
|
||||
}
|
||||
content: 'An important title!',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = new Vue({
|
||||
@@ -460,12 +448,12 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: chunk => `${chunk} - My page`
|
||||
}
|
||||
]
|
||||
template: chunk => `${chunk} - My page`,
|
||||
},
|
||||
],
|
||||
},
|
||||
el: document.createElement('div'),
|
||||
render: h => h('div', null, [h('merge-child')])
|
||||
render: h => h('div', null, [h('merge-child')]),
|
||||
})
|
||||
|
||||
const expectedMetaInfo = {
|
||||
@@ -480,8 +468,8 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'An important title! - My page',
|
||||
template: true
|
||||
}
|
||||
template: true,
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -489,7 +477,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
}
|
||||
|
||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||
@@ -505,10 +493,10 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
template: chunk => `${chunk} - My page`
|
||||
}
|
||||
]
|
||||
}
|
||||
template: chunk => `${chunk} - My page`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = new Vue({
|
||||
@@ -518,12 +506,12 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: chunk => `${chunk} - SHOULD NEVER HAPPEN`
|
||||
}
|
||||
]
|
||||
template: chunk => `${chunk} - SHOULD NEVER HAPPEN`,
|
||||
},
|
||||
],
|
||||
},
|
||||
el: document.createElement('div'),
|
||||
render: h => h('div', null, [h('merge-child')])
|
||||
render: h => h('div', null, [h('merge-child')]),
|
||||
})
|
||||
|
||||
const expectedMetaInfo = {
|
||||
@@ -538,8 +526,8 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title - My page',
|
||||
template: true
|
||||
}
|
||||
template: true,
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -547,7 +535,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
}
|
||||
|
||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||
@@ -564,10 +552,10 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'An important title!',
|
||||
template: chunk => `${chunk} - My page`
|
||||
}
|
||||
]
|
||||
}
|
||||
template: chunk => `${chunk} - My page`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = new Vue({
|
||||
@@ -577,12 +565,12 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: chunk => `${chunk} - SHOULD NEVER HAPPEN`
|
||||
}
|
||||
]
|
||||
template: chunk => `${chunk} - SHOULD NEVER HAPPEN`,
|
||||
},
|
||||
],
|
||||
},
|
||||
el: document.createElement('div'),
|
||||
render: h => h('div', null, [h('merge-child')])
|
||||
render: h => h('div', null, [h('merge-child')]),
|
||||
})
|
||||
|
||||
const expectedMetaInfo = {
|
||||
@@ -597,8 +585,8 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'An important title! - My page',
|
||||
template: true
|
||||
}
|
||||
template: true,
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -606,7 +594,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
}
|
||||
|
||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||
@@ -623,10 +611,10 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'An important title!',
|
||||
template: chunk => `${chunk} - My page`
|
||||
}
|
||||
]
|
||||
}
|
||||
template: chunk => `${chunk} - My page`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = new Vue({
|
||||
@@ -635,12 +623,12 @@ describe('getMetaInfo', () => {
|
||||
{
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title'
|
||||
}
|
||||
]
|
||||
content: 'Test title',
|
||||
},
|
||||
],
|
||||
},
|
||||
el: document.createElement('div'),
|
||||
render: h => h('div', null, [h('merge-child')])
|
||||
render: h => h('div', null, [h('merge-child')]),
|
||||
})
|
||||
|
||||
const expectedMetaInfo = {
|
||||
@@ -655,8 +643,8 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'An important title! - My page',
|
||||
template: true
|
||||
}
|
||||
template: true,
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -664,7 +652,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
}
|
||||
|
||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||
@@ -673,9 +661,9 @@ describe('getMetaInfo', () => {
|
||||
|
||||
test('no errors when metaInfo returns nothing', () => {
|
||||
const component = new Vue({
|
||||
metaInfo () {},
|
||||
metaInfo() {},
|
||||
el: document.createElement('div'),
|
||||
render: h => h('div', null, [])
|
||||
render: h => h('div', null, []),
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -692,7 +680,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -702,34 +690,34 @@ describe('getMetaInfo', () => {
|
||||
metaInfo: {
|
||||
title: undefined,
|
||||
bodyAttrs: {
|
||||
class: undefined
|
||||
class: undefined,
|
||||
},
|
||||
meta: [
|
||||
{
|
||||
vmid: 'og:title',
|
||||
content: undefined
|
||||
}
|
||||
]
|
||||
}
|
||||
content: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
bodyAttrs: {
|
||||
class: 'class'
|
||||
class: 'class',
|
||||
},
|
||||
meta: [
|
||||
{
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: chunk => `${chunk} - My page`
|
||||
}
|
||||
]
|
||||
template: chunk => `${chunk} - My page`,
|
||||
},
|
||||
],
|
||||
},
|
||||
el: document.createElement('div'),
|
||||
render: h => h('div', null, [h('merge-child')])
|
||||
render: h => h('div', null, [h('merge-child')]),
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -737,7 +725,7 @@ describe('getMetaInfo', () => {
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate: '%s',
|
||||
bodyAttrs: {
|
||||
class: 'class'
|
||||
class: 'class',
|
||||
},
|
||||
headAttrs: {},
|
||||
htmlAttrs: {},
|
||||
@@ -746,8 +734,8 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title - My page',
|
||||
template: true
|
||||
}
|
||||
template: true,
|
||||
},
|
||||
],
|
||||
base: [],
|
||||
link: [],
|
||||
@@ -755,7 +743,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -767,10 +755,10 @@ describe('getMetaInfo', () => {
|
||||
meta: [
|
||||
{
|
||||
vmid: 'og:title',
|
||||
content: null
|
||||
}
|
||||
]
|
||||
}
|
||||
content: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const component = new Vue({
|
||||
@@ -781,12 +769,12 @@ describe('getMetaInfo', () => {
|
||||
vmid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: 'Test title',
|
||||
template: chunk => `${chunk} - My page`
|
||||
}
|
||||
]
|
||||
template: chunk => `${chunk} - My page`,
|
||||
},
|
||||
],
|
||||
},
|
||||
el: document.createElement('div'),
|
||||
render: h => h('div', null, [h('merge-child')])
|
||||
render: h => h('div', null, [h('merge-child')]),
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -803,7 +791,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -812,19 +800,19 @@ describe('getMetaInfo', () => {
|
||||
render: h => h('div'),
|
||||
metaInfo: {
|
||||
bodyAttrs: {
|
||||
class: ['foo']
|
||||
}
|
||||
}
|
||||
class: ['foo'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
bodyAttrs: {
|
||||
class: ['bar']
|
||||
}
|
||||
class: ['bar'],
|
||||
},
|
||||
},
|
||||
el: document.createElement('div'),
|
||||
render: h => h('div', null, [h('merge-child')])
|
||||
render: h => h('div', null, [h('merge-child')]),
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -834,7 +822,7 @@ describe('getMetaInfo', () => {
|
||||
htmlAttrs: {},
|
||||
headAttrs: {},
|
||||
bodyAttrs: {
|
||||
class: ['bar', 'foo']
|
||||
class: ['bar', 'foo'],
|
||||
},
|
||||
meta: [],
|
||||
base: [],
|
||||
@@ -843,7 +831,7 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -853,9 +841,9 @@ describe('getMetaInfo', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
htmlAttrs: {
|
||||
amp: undefined
|
||||
}
|
||||
}
|
||||
amp: undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
@@ -872,10 +860,12 @@ describe('getMetaInfo', () => {
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
__dangerouslyDisableSanitizersByTagID: {},
|
||||
})
|
||||
|
||||
expect(warn).toHaveBeenCalledTimes(1)
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringMatching('the value undefined'))
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringMatching('the value undefined')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
+27
-14
@@ -2,7 +2,7 @@ import { pTick, createDOM } from '../utils'
|
||||
|
||||
const onLoadAttribute = {
|
||||
k: 'onload',
|
||||
v: 'this.__vm_l=1'
|
||||
v: 'this.__vm_l=1',
|
||||
}
|
||||
|
||||
const getLoadAttribute = () => `${onLoadAttribute.k}="${onLoadAttribute.v}"`
|
||||
@@ -84,7 +84,9 @@ describe('load callbacks', () => {
|
||||
})
|
||||
|
||||
test('addCallbacks', () => {
|
||||
const addListeners = jest.spyOn(document, 'querySelectorAll').mockReturnValue(false)
|
||||
const addListeners = jest
|
||||
.spyOn(document, 'querySelectorAll')
|
||||
.mockReturnValue(false)
|
||||
|
||||
const config = { tagIDKeyName: 'test-id' }
|
||||
|
||||
@@ -92,7 +94,7 @@ describe('load callbacks', () => {
|
||||
{ [config.tagIDKeyName]: 'test1', callback: false },
|
||||
{ [config.tagIDKeyName]: false, callback: () => {} },
|
||||
{ [config.tagIDKeyName]: 'test1', callback: () => {} },
|
||||
{ [config.tagIDKeyName]: 'test2', callback: () => {} }
|
||||
{ [config.tagIDKeyName]: 'test2', callback: () => {} },
|
||||
]
|
||||
|
||||
load.addCallbacks(config, 'link', tags)
|
||||
@@ -101,20 +103,27 @@ describe('load callbacks', () => {
|
||||
load.applyCallbacks({ matches })
|
||||
|
||||
expect(matches).toHaveBeenCalledTimes(2)
|
||||
expect(matches).toHaveBeenCalledWith(`link[data-${config.tagIDKeyName}="test1"][${getLoadAttribute()}]`)
|
||||
expect(matches).toHaveBeenCalledWith(`link[data-${config.tagIDKeyName}="test2"][${getLoadAttribute()}]`)
|
||||
expect(matches).toHaveBeenCalledWith(
|
||||
`link[data-${config.tagIDKeyName}="test1"][${getLoadAttribute()}]`
|
||||
)
|
||||
expect(matches).toHaveBeenCalledWith(
|
||||
`link[data-${config.tagIDKeyName}="test2"][${getLoadAttribute()}]`
|
||||
)
|
||||
|
||||
expect(addListeners).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('addCallbacks (auto add listeners)', () => {
|
||||
const addListeners = jest.spyOn(document, 'querySelectorAll').mockReturnValue(false)
|
||||
const addListeners = jest
|
||||
.spyOn(document, 'querySelectorAll')
|
||||
.mockReturnValue(false)
|
||||
|
||||
const config = { tagIDKeyName: 'test-id', loadCallbackAttribute: 'test-load' }
|
||||
const config = {
|
||||
tagIDKeyName: 'test-id',
|
||||
loadCallbackAttribute: 'test-load',
|
||||
}
|
||||
|
||||
const tags = [
|
||||
{ [config.tagIDKeyName]: 'test1', callback: () => {} }
|
||||
]
|
||||
const tags = [{ [config.tagIDKeyName]: 'test1', callback: () => {} }]
|
||||
|
||||
load.addCallbacks(config, 'style', tags, true)
|
||||
|
||||
@@ -122,7 +131,9 @@ describe('load callbacks', () => {
|
||||
load.applyCallbacks({ matches })
|
||||
|
||||
expect(matches).toHaveBeenCalledTimes(1)
|
||||
expect(matches).toHaveBeenCalledWith(`style[data-${config.tagIDKeyName}="test1"][${getLoadAttribute()}]`)
|
||||
expect(matches).toHaveBeenCalledWith(
|
||||
`style[data-${config.tagIDKeyName}="test1"][${getLoadAttribute()}]`
|
||||
)
|
||||
|
||||
expect(addListeners).toHaveBeenCalled()
|
||||
})
|
||||
@@ -188,9 +199,11 @@ describe('load callbacks', () => {
|
||||
|
||||
const el = document.createElement('script')
|
||||
const addEventListener = el.addEventListener.bind(el)
|
||||
const addEventListenerSpy = jest.spyOn(el, 'addEventListener').mockImplementation((...args) => {
|
||||
return addEventListener(...args)
|
||||
})
|
||||
const addEventListenerSpy = jest
|
||||
.spyOn(el, 'addEventListener')
|
||||
.mockImplementation((...args) => {
|
||||
return addEventListener(...args)
|
||||
})
|
||||
el.setAttribute(onLoadAttribute.k, onLoadAttribute.v)
|
||||
document.body.appendChild(el)
|
||||
|
||||
|
||||
+43
-33
@@ -4,7 +4,7 @@ import { defaultOptions } from '../../src/shared/constants'
|
||||
|
||||
jest.mock('../../src/client/update')
|
||||
jest.mock('../../package.json', () => ({
|
||||
version: 'test-version'
|
||||
version: 'test-version',
|
||||
}))
|
||||
|
||||
describe('plugin', () => {
|
||||
@@ -56,8 +56,8 @@ describe('plugin', () => {
|
||||
const Component = Vue.component('test-component', {
|
||||
template: '<div>Test</div>',
|
||||
[defaultOptions.keyName]: {
|
||||
title: 'Hello World'
|
||||
}
|
||||
title: 'Hello World',
|
||||
},
|
||||
})
|
||||
|
||||
const { vm } = mount(Component, { localVue: Vue })
|
||||
@@ -86,8 +86,8 @@ describe('plugin', () => {
|
||||
const Component = Vue.component('test-component', {
|
||||
template: '<div>Test</div>',
|
||||
[defaultOptions.keyName]: {
|
||||
title: 'Hello World'
|
||||
}
|
||||
title: 'Hello World',
|
||||
},
|
||||
})
|
||||
|
||||
Vue.config.devtools = true
|
||||
@@ -107,8 +107,8 @@ describe('plugin', () => {
|
||||
const Component = Vue.component('test-component', {
|
||||
template: '<div>Test</div>',
|
||||
[defaultOptions.keyName]: {
|
||||
title: 'Hello World'
|
||||
}
|
||||
title: 'Hello World',
|
||||
},
|
||||
})
|
||||
|
||||
const { vm } = mount(Component, { localVue: Vue })
|
||||
@@ -122,13 +122,15 @@ describe('plugin', () => {
|
||||
test('can use generate export with options', () => {
|
||||
process.server = true
|
||||
const rawInfo = {
|
||||
meta: [{ charset: 'utf-8' }]
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
}
|
||||
|
||||
const metaInfo = VueMetaPlugin.generate(rawInfo, {
|
||||
ssrAppId: 'my-test-app-id'
|
||||
ssrAppId: 'my-test-app-id',
|
||||
})
|
||||
expect(metaInfo.meta.text()).toBe('<meta data-vue-meta="my-test-app-id" charset="utf-8">')
|
||||
expect(metaInfo.meta.text()).toBe(
|
||||
'<meta data-vue-meta="my-test-app-id" charset="utf-8">'
|
||||
)
|
||||
|
||||
// no error on not provided metaInfo types
|
||||
expect(metaInfo.script.text()).toBe('')
|
||||
@@ -139,50 +141,56 @@ describe('plugin', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
const rawInfo = {
|
||||
meta: [{ charset: 'utf-8' }]
|
||||
meta: [{ charset: 'utf-8' }],
|
||||
}
|
||||
|
||||
const metaInfo = VueMetaPlugin.generate(rawInfo)
|
||||
expect(metaInfo).toBeUndefined()
|
||||
expect(warn).toHaveBeenCalledTimes(1)
|
||||
expect(warn).toHaveBeenCalledWith('generate is not supported in browser builds')
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'generate is not supported in browser builds'
|
||||
)
|
||||
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
test('updates can be paused and resumed', async () => {
|
||||
const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update')
|
||||
const { batchUpdate: _batchUpdate } = jest.requireActual(
|
||||
'../../src/client/update'
|
||||
)
|
||||
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
|
||||
// because triggerUpdate & batchUpdate reside in the same file we cant mock them both,
|
||||
// so just recreate the triggerUpdate fn by copying its implementation
|
||||
const triggerUpdateSpy = triggerUpdate.mockImplementation((options, vm, hookName) => {
|
||||
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.pausing) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchUpdateSpy(() => vm.$meta().refresh())
|
||||
const triggerUpdateSpy = triggerUpdate.mockImplementation(
|
||||
(options, vm, hookName) => {
|
||||
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.pausing) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchUpdateSpy(() => vm.$meta().refresh())
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const Component = Vue.component('test-component', {
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title
|
||||
title: this.title,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
template: '<div>Test</div>'
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
|
||||
let title = 'first title'
|
||||
const wrapper = mount(Component, {
|
||||
localVue: Vue,
|
||||
propsData: {
|
||||
title
|
||||
}
|
||||
title,
|
||||
},
|
||||
})
|
||||
|
||||
// no batchUpdate on initialization
|
||||
@@ -223,7 +231,9 @@ describe('plugin', () => {
|
||||
test('updates are batched by default', async () => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update')
|
||||
const { batchUpdate: _batchUpdate } = jest.requireActual(
|
||||
'../../src/client/update'
|
||||
)
|
||||
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
|
||||
const refreshSpy = jest.fn()
|
||||
// because triggerUpdate & batchUpdate reside in the same file we cant mock them both,
|
||||
@@ -236,26 +246,26 @@ describe('plugin', () => {
|
||||
})
|
||||
|
||||
const Component = Vue.component('test-component', {
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title
|
||||
title: this.title,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
template: '<div>Test</div>'
|
||||
template: '<div>Test</div>',
|
||||
})
|
||||
|
||||
let title = 'first title'
|
||||
const wrapper = mount(Component, {
|
||||
localVue: Vue,
|
||||
propsData: {
|
||||
title
|
||||
}
|
||||
title,
|
||||
},
|
||||
})
|
||||
await vmTick(wrapper.vm)
|
||||
jest.clearAllMocks()
|
||||
|
||||
@@ -20,9 +20,9 @@ describe('shared', () => {
|
||||
const componentMock = {
|
||||
_vueMeta: {
|
||||
initialized: true,
|
||||
pausing: false
|
||||
pausing: false,
|
||||
},
|
||||
$meta: () => ({ refresh })
|
||||
$meta: () => ({ refresh }),
|
||||
}
|
||||
|
||||
triggerUpdate({ debounceWait: 0 }, componentMock, 'test')
|
||||
|
||||
+31
-12
@@ -1,10 +1,15 @@
|
||||
import _updateClientMetaInfo from '../../src/client/updateClientMetaInfo'
|
||||
import { defaultOptions, ssrAppId, ssrAttribute } from '../../src/shared/constants'
|
||||
import {
|
||||
defaultOptions,
|
||||
ssrAppId,
|
||||
ssrAttribute,
|
||||
} from '../../src/shared/constants'
|
||||
import metaInfoData from '../utils/meta-info-data'
|
||||
import * as load from '../../src/client/load'
|
||||
import { clearClientAttributeMap } from '../utils'
|
||||
|
||||
const updateClientMetaInfo = (type, data) => _updateClientMetaInfo(ssrAppId, defaultOptions, { [type]: data })
|
||||
const updateClientMetaInfo = (type, data) =>
|
||||
_updateClientMetaInfo(ssrAppId, defaultOptions, { [type]: data })
|
||||
|
||||
describe('updaters', () => {
|
||||
let html
|
||||
@@ -13,22 +18,26 @@ describe('updaters', () => {
|
||||
html = document.getElementsByTagName('html')[0]
|
||||
|
||||
// remove default meta charset
|
||||
Array.from(html.getElementsByTagName('meta')).forEach(el => el.parentNode.removeChild(el))
|
||||
Array.from(html.getElementsByTagName('meta')).forEach(el =>
|
||||
el.parentNode.removeChild(el)
|
||||
)
|
||||
})
|
||||
|
||||
for (const type in metaInfoData) {
|
||||
const typeTests = metaInfoData[type]
|
||||
|
||||
const testCases = {
|
||||
add: (tags) => {
|
||||
add: tags => {
|
||||
typeTests.add.expect.forEach((expected, index) => {
|
||||
if (!['title', 'htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||
if (
|
||||
!['title', 'htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)
|
||||
) {
|
||||
expect(tags.tagsAdded[type][index].outerHTML).toBe(expected)
|
||||
}
|
||||
expect(html.outerHTML).toContain(expected)
|
||||
})
|
||||
},
|
||||
change: (tags) => {
|
||||
change: tags => {
|
||||
typeTests.add.expect.forEach((expected, index) => {
|
||||
if (!typeTests.change.expect.includes(expected)) {
|
||||
expect(html.outerHTML).not.toContain(expected)
|
||||
@@ -36,13 +45,15 @@ describe('updaters', () => {
|
||||
})
|
||||
|
||||
typeTests.change.expect.forEach((expected, index) => {
|
||||
if (!['title', 'htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||
if (
|
||||
!['title', 'htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)
|
||||
) {
|
||||
expect(tags.tagsAdded[type][index].outerHTML).toBe(expected)
|
||||
}
|
||||
expect(html.outerHTML).toContain(expected)
|
||||
})
|
||||
},
|
||||
remove: (tags) => {
|
||||
remove: tags => {
|
||||
// TODO: i'd expect tags.removedTags to be populated
|
||||
typeTests.add.expect.forEach((expected, index) => {
|
||||
expect(html.outerHTML).not.toContain(expected)
|
||||
@@ -53,13 +64,13 @@ describe('updaters', () => {
|
||||
})
|
||||
|
||||
expect(html.outerHTML).not.toContain(`<${type}`)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
describe(`${type} type tests`, () => {
|
||||
beforeAll(() => clearClientAttributeMap())
|
||||
|
||||
Object.keys(typeTests).forEach((action) => {
|
||||
Object.keys(typeTests).forEach(action => {
|
||||
const testInfo = typeTests[action]
|
||||
|
||||
// return when no test case available
|
||||
@@ -102,12 +113,20 @@ describe('updaters', () => {
|
||||
describe('extra tests', () => {
|
||||
test('adds callback listener on hydration', () => {
|
||||
const addListeners = load.addListeners
|
||||
const addListenersSpy = jest.spyOn(load, 'addListeners').mockImplementation(addListeners)
|
||||
const addListenersSpy = jest
|
||||
.spyOn(load, 'addListeners')
|
||||
.mockImplementation(addListeners)
|
||||
|
||||
const html = document.getElementsByTagName('html')[0]
|
||||
html.setAttribute(ssrAttribute, 'true')
|
||||
|
||||
const data = [{ src: 'src1', [defaultOptions.tagIDKeyName]: 'content', callback: () => {} }]
|
||||
const data = [
|
||||
{
|
||||
src: 'src1',
|
||||
[defaultOptions.tagIDKeyName]: 'content',
|
||||
callback: () => {},
|
||||
},
|
||||
]
|
||||
const tags = updateClientMetaInfo('script', data)
|
||||
|
||||
expect(tags).toBe(false)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user