mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-24 15:00:33 +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
|
node_modules
|
||||||
__build__
|
__build__
|
||||||
dist
|
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',
|
zipcode: '92998-3874',
|
||||||
geo: {
|
geo: {
|
||||||
lat: '-37.3159',
|
lat: '-37.3159',
|
||||||
lng: '81.1496'
|
lng: '81.1496',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
phone: '1-770-736-8031 x56442',
|
phone: '1-770-736-8031 x56442',
|
||||||
website: 'hildegard.org',
|
website: 'hildegard.org',
|
||||||
company: {
|
company: {
|
||||||
name: 'Romaguera-Crona',
|
name: 'Romaguera-Crona',
|
||||||
catchPhrase: 'Multi-layered client-server neural-net',
|
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',
|
zipcode: '90566-7771',
|
||||||
geo: {
|
geo: {
|
||||||
lat: '-43.9509',
|
lat: '-43.9509',
|
||||||
lng: '-34.4618'
|
lng: '-34.4618',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
phone: '010-692-6593 x09125',
|
phone: '010-692-6593 x09125',
|
||||||
website: 'anastasia.net',
|
website: 'anastasia.net',
|
||||||
company: {
|
company: {
|
||||||
name: 'Deckow-Crist',
|
name: 'Deckow-Crist',
|
||||||
catchPhrase: 'Proactive didactic contingency',
|
catchPhrase: 'Proactive didactic contingency',
|
||||||
bs: 'synergize scalable supply-chains'
|
bs: 'synergize scalable supply-chains',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ window.users.push({
|
|||||||
zipcode: '59590-4157',
|
zipcode: '59590-4157',
|
||||||
geo: {
|
geo: {
|
||||||
lat: '-68.6102',
|
lat: '-68.6102',
|
||||||
lng: '-47.0653'
|
lng: '-47.0653',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
phone: '1-463-123-4447',
|
phone: '1-463-123-4447',
|
||||||
website: 'ramiro.info',
|
website: 'ramiro.info',
|
||||||
company: {
|
company: {
|
||||||
name: 'Romaguera-Jacobson',
|
name: 'Romaguera-Jacobson',
|
||||||
catchPhrase: 'Face to face bifurcated interface',
|
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',
|
zipcode: '53919-4257',
|
||||||
geo: {
|
geo: {
|
||||||
lat: '29.4572',
|
lat: '29.4572',
|
||||||
lng: '-164.2990'
|
lng: '-164.2990',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
phone: '493-170-9623 x156',
|
phone: '493-170-9623 x156',
|
||||||
website: 'kale.biz',
|
website: 'kale.biz',
|
||||||
company: {
|
company: {
|
||||||
name: 'Robel-Corkery',
|
name: 'Robel-Corkery',
|
||||||
catchPhrase: 'Multi-tiered zero tolerance productivity',
|
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 = []
|
window.users = []
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
metaInfo () {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
title: 'Async Callback',
|
title: 'Async Callback',
|
||||||
titleTemplate: '%s | Vue Meta Examples',
|
titleTemplate: '%s | Vue Meta Examples',
|
||||||
@@ -16,56 +16,56 @@ new Vue({
|
|||||||
vmid: 'potatoes',
|
vmid: 'potatoes',
|
||||||
src: '/user-3.js',
|
src: '/user-3.js',
|
||||||
async: true,
|
async: true,
|
||||||
callback: this.updateCounter
|
callback: this.updateCounter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
skip: this.count < 1,
|
skip: this.count < 1,
|
||||||
vmid: 'vegetables',
|
vmid: 'vegetables',
|
||||||
src: '/user-2.js',
|
src: '/user-2.js',
|
||||||
async: true,
|
async: true,
|
||||||
callback: this.updateCounter
|
callback: this.updateCounter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
vmid: 'meat',
|
vmid: 'meat',
|
||||||
src: '/user-1.js',
|
src: '/user-1.js',
|
||||||
async: true,
|
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 {
|
return {
|
||||||
count: 0,
|
count: 0,
|
||||||
scripts: [],
|
scripts: [],
|
||||||
users: window.users
|
users: window.users,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
count (val) {
|
count(val) {
|
||||||
if (val === 3) {
|
if (val === 3) {
|
||||||
this.addScript()
|
this.addScript()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateCounter () {
|
updateCounter() {
|
||||||
this.count++
|
this.count++
|
||||||
},
|
},
|
||||||
addScript () {
|
addScript() {
|
||||||
this.scripts.push({
|
this.scripts.push({
|
||||||
src: '/user-4.js',
|
src: '/user-4.js',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
this.updateCounter()
|
this.updateCounter()
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadCallback (vmid) {
|
loadCallback(vmid) {
|
||||||
if (vmid === 'meat') {
|
if (vmid === 'meat') {
|
||||||
this.updateCounter()
|
this.updateCounter()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -84,5 +84,5 @@ new Vue({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`,
|
||||||
}).$mount('#app')
|
}).$mount('#app')
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ Vue.component('child', {
|
|||||||
props: {
|
props: {
|
||||||
page: {
|
page: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
render (h) {
|
render(h) {
|
||||||
return h('h3', null, this.page)
|
return h('h3', null, this.page)
|
||||||
},
|
},
|
||||||
metaInfo () {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
title: this.page
|
title: this.page,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
@@ -28,5 +28,5 @@ new Vue({
|
|||||||
<p>Inspect Element to see the meta info</p>
|
<p>Inspect Element to see the meta info</p>
|
||||||
<child page="This is a prop"></child>
|
<child page="This is a prop"></child>
|
||||||
</div>
|
</div>
|
||||||
`
|
`,
|
||||||
}).$mount('#app')
|
}).$mount('#app')
|
||||||
|
|||||||
+15
-9
@@ -15,18 +15,24 @@ new Vue({
|
|||||||
titleTemplate: '%s | Vue Meta Examples',
|
titleTemplate: '%s | Vue Meta Examples',
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
amp: undefined
|
amp: undefined,
|
||||||
},
|
},
|
||||||
headAttrs: {
|
headAttrs: {
|
||||||
test: true
|
test: true,
|
||||||
},
|
},
|
||||||
meta: [
|
meta: [{ name: 'description', content: 'Hello', vmid: 'test' }],
|
||||||
{ name: 'description', content: 'Hello', vmid: 'test' }
|
|
||||||
],
|
|
||||||
script: [
|
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')
|
}).$mount('#app')
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ Vue.use(VueMeta)
|
|||||||
Vue.component('foo', {
|
Vue.component('foo', {
|
||||||
template: '<p>Foo component</p>',
|
template: '<p>Foo component</p>',
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Keep me Foo'
|
title: 'Keep me Foo',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
data () {
|
data() {
|
||||||
return { showFoo: false }
|
return { showFoo: false }
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
show () {
|
show() {
|
||||||
this.showFoo = !this.showFoo
|
this.showFoo = !this.showFoo
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -29,6 +29,6 @@ new Vue({
|
|||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
metaInfo: () => ({
|
metaInfo: () => ({
|
||||||
title: 'Keep-alive'
|
title: 'Keep-alive',
|
||||||
})
|
}),
|
||||||
}).$mount('#app')
|
}).$mount('#app')
|
||||||
|
|||||||
+11
-10
@@ -5,12 +5,12 @@ const {
|
|||||||
processIf,
|
processIf,
|
||||||
getBaseTransformPreset,
|
getBaseTransformPreset,
|
||||||
createObjectExpression,
|
createObjectExpression,
|
||||||
createObjectProperty
|
createObjectProperty,
|
||||||
} = require('@vue/compiler-core')
|
} = require('@vue/compiler-core')
|
||||||
|
|
||||||
const { parse } = require('@vue/compiler-dom')
|
const { parse } = require('@vue/compiler-dom')
|
||||||
|
|
||||||
function headTransform (node, context) {
|
function headTransform(node, context) {
|
||||||
console.log('NODE', node)
|
console.log('NODE', node)
|
||||||
if (node.type === 1 /* NodeTypes.ELEMENT */) {
|
if (node.type === 1 /* NodeTypes.ELEMENT */) {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -35,23 +35,22 @@ module.exports = function (source, map) {
|
|||||||
// console.log('AST', ast)
|
// console.log('AST', ast)
|
||||||
|
|
||||||
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset({
|
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset({
|
||||||
prefixIdentifiers: true
|
prefixIdentifiers: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
transform(ast, {
|
transform(ast, {
|
||||||
prefixIdentifiers: true,
|
prefixIdentifiers: true,
|
||||||
nodeTransforms: [
|
nodeTransforms: [...nodeTransforms, headTransform],
|
||||||
...nodeTransforms,
|
directiveTransforms,
|
||||||
headTransform
|
|
||||||
],
|
|
||||||
directiveTransforms
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = generate(ast, { mode: 'module' })
|
const result = generate(ast, { mode: 'module' })
|
||||||
|
|
||||||
console.log(result.code)
|
console.log(result.code)
|
||||||
|
|
||||||
this.callback(null, `
|
this.callback(
|
||||||
|
null,
|
||||||
|
`
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
${result.code}
|
${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
|
// index.html contains a manual SSR render
|
||||||
|
|
||||||
const app1 = new Vue({
|
const app1 = new Vue({
|
||||||
metaInfo () {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
title: 'App 1 title',
|
title: 'App 1 title',
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: 'app-1'
|
class: 'app-1',
|
||||||
},
|
},
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'description', content: 'Hello from app 1', vmid: 'test' },
|
{ name: 'description', content: 'Hello from app 1', vmid: 'test' },
|
||||||
{ name: 'og:description', content: this.ogContent }
|
{ name: 'og:description', content: this.ogContent },
|
||||||
],
|
],
|
||||||
script: [
|
script: [
|
||||||
{ innerHTML: 'var appId=1.1', body: true },
|
{ 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 {
|
return {
|
||||||
ogContent: 'Hello from ssr app'
|
ogContent: 'Hello from ssr app',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div id="app1"><h1>App 1</h1></div>
|
<div id="app1"><h1>App 1</h1></div>
|
||||||
`
|
`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const app2 = new Vue({
|
const app2 = new Vue({
|
||||||
metaInfo: () => ({
|
metaInfo: () => ({
|
||||||
title: 'App 2 title',
|
title: 'App 2 title',
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: 'app-2'
|
class: 'app-2',
|
||||||
},
|
},
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'description', content: 'Hello from app 2', vmid: 'test' },
|
{ 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: [
|
script: [
|
||||||
{ innerHTML: 'var appId=2.1', body: true },
|
{ 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: `
|
template: `
|
||||||
<div id="app2"><h1>App 2</h1></div>
|
<div id="app2"><h1>App 2</h1></div>
|
||||||
`
|
`,
|
||||||
}).$mount('#app2')
|
}).$mount('#app2')
|
||||||
|
|
||||||
app1.$mount('#app1')
|
app1.$mount('#app1')
|
||||||
@@ -57,7 +57,7 @@ app1.$mount('#app1')
|
|||||||
const app3 = new Vue({
|
const app3 = new Vue({
|
||||||
template: `
|
template: `
|
||||||
<div id="app3"><h1>App 3 (empty metaInfo)</h1></div>
|
<div id="app3"><h1>App 3 (empty metaInfo)</h1></div>
|
||||||
`
|
`,
|
||||||
}).$mount('#app3')
|
}).$mount('#app3')
|
||||||
|
|
||||||
setTimeout(() => {
|
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()
|
const app = express()
|
||||||
|
|
||||||
app.use(webpackDevMiddleware(webpack(WebpackConfig), {
|
app.use(
|
||||||
publicPath: '/__build__/',
|
webpackDevMiddleware(webpack(WebpackConfig), {
|
||||||
writeToDisk: true,
|
publicPath: '/__build__/',
|
||||||
stats: {
|
writeToDisk: true,
|
||||||
colors: true,
|
stats: {
|
||||||
chunks: false
|
colors: true,
|
||||||
}
|
chunks: false,
|
||||||
}))
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
fs.readdirSync(__dirname)
|
||||||
.filter(file => file !== 'ssr')
|
.filter(file => file !== 'ssr')
|
||||||
.forEach((file) => {
|
.forEach(file => {
|
||||||
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
|
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
|
||||||
app.use(rewrite(`/${file}/*`, `/${file}/index.html`))
|
app.use(rewrite(`/${file}/*`, `/${file}/index.html`))
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-35
@@ -6,7 +6,7 @@ Vue.use(VueMeta, {
|
|||||||
tagIDKeyName: 'hid'
|
tagIDKeyName: 'hid'
|
||||||
}) */
|
}) */
|
||||||
|
|
||||||
export default function createMyApp () {
|
export default function createMyApp() {
|
||||||
const Home = {
|
const Home = {
|
||||||
template: `<div>
|
template: `<div>
|
||||||
<router-link to="/about">About</router-link>
|
<router-link to="/about">About</router-link>
|
||||||
@@ -19,15 +19,15 @@ export default function createMyApp () {
|
|||||||
{
|
{
|
||||||
hid: 'og:title',
|
hid: 'og:title',
|
||||||
name: 'og:title',
|
name: 'og:title',
|
||||||
content: 'Hello World'
|
content: 'Hello World',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hid: 'description',
|
hid: 'description',
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content: 'Hello World'
|
content: 'Hello World',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const About = {
|
const About = {
|
||||||
@@ -42,28 +42,28 @@ export default function createMyApp () {
|
|||||||
{
|
{
|
||||||
hid: 'og:title',
|
hid: 'og:title',
|
||||||
name: 'og:title',
|
name: 'og:title',
|
||||||
content: 'About World'
|
content: 'About World',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hid: 'description',
|
hid: 'description',
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content: 'About World'
|
content: 'About World',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createMemoryHistory('/ssr'),
|
history: createMemoryHistory('/ssr'),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: Home },
|
{ path: '/', component: Home },
|
||||||
{ path: '/about', component: About }
|
{ path: '/about', component: About },
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const app = createSSRApp({
|
const app = createSSRApp({
|
||||||
router,
|
router,
|
||||||
metaInfo () {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
title: 'Boring Title',
|
title: 'Boring Title',
|
||||||
htmlAttrs: { amp: true },
|
htmlAttrs: { amp: true },
|
||||||
@@ -74,59 +74,63 @@ export default function createMyApp () {
|
|||||||
hid: 'og:title',
|
hid: 'og:title',
|
||||||
name: 'og:title',
|
name: 'og:title',
|
||||||
template: chunk => `${chunk} - My Site`,
|
template: chunk => `${chunk} - My Site`,
|
||||||
content: 'Default Title'
|
content: 'Default Title',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hid: 'description',
|
hid: 'description',
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content: 'Say something'
|
content: 'Say something',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
hid: 'ldjson-schema',
|
hid: 'ldjson-schema',
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }'
|
innerHTML:
|
||||||
}, {
|
'{ "@context": "http://www.schema.org", "@type": "Organization" }',
|
||||||
|
},
|
||||||
|
{
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
innerHTML: '{ "body": "yes" }',
|
innerHTML: '{ "body": "yes" }',
|
||||||
body: true
|
body: true,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
hid: 'my-async-script-with-load-callback',
|
hid: 'my-async-script-with-load-callback',
|
||||||
src: '/user-1.js',
|
src: '/user-1.js',
|
||||||
body: true,
|
body: true,
|
||||||
defer: true,
|
defer: true,
|
||||||
callback: this.loadCallback
|
callback: this.loadCallback,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
skip: this.count < 1,
|
skip: this.count < 1,
|
||||||
src: '/user-2.js',
|
src: '/user-2.js',
|
||||||
body: true,
|
body: true,
|
||||||
callback: this.loadCallback
|
callback: this.loadCallback,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
__dangerouslyDisableSanitizersByTagID: {
|
__dangerouslyDisableSanitizersByTagID: {
|
||||||
'ldjson-schema': ['innerHTML']
|
'ldjson-schema': ['innerHTML'],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
count: 0,
|
count: 0,
|
||||||
users: process.server ? [] : window.users
|
users: process.server ? [] : window.users,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted() {
|
||||||
const { set, remove } = this.$meta().addApp('client-only')
|
const { set, remove } = this.$meta().addApp('client-only')
|
||||||
set({
|
set({
|
||||||
bodyAttrs: { class: 'client-only' }
|
bodyAttrs: { class: 'client-only' },
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => remove(), 3000)
|
setTimeout(() => remove(), 3000)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadCallback () {
|
loadCallback() {
|
||||||
this.count++
|
this.count++
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -142,8 +146,7 @@ export default function createMyApp () {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>`
|
</div>`,
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
|
|||||||
|
|
||||||
process.server = true
|
process.server = true
|
||||||
|
|
||||||
export async function renderPage ({ url }) {
|
export async function renderPage({ url }) {
|
||||||
const { app, router } = await createApp()
|
const { app, router } = await createApp()
|
||||||
|
|
||||||
await router.push(url.substr(4))
|
await router.push(url.substr(4))
|
||||||
@@ -30,17 +30,17 @@ export async function renderPage ({ url }) {
|
|||||||
const pageHtml = compiled({
|
const pageHtml = compiled({
|
||||||
app: appHtml,
|
app: appHtml,
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
text: () => {}
|
text: () => {},
|
||||||
},
|
},
|
||||||
headAttrs: {
|
headAttrs: {
|
||||||
text: () => {}
|
text: () => {},
|
||||||
},
|
},
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
text: () => {}
|
text: () => {},
|
||||||
},
|
},
|
||||||
head: () => {},
|
head: () => {},
|
||||||
bodyPrepend: () => {},
|
bodyPrepend: () => {},
|
||||||
bodyAppend: () => {}
|
bodyAppend: () => {},
|
||||||
// ...app.$meta().inject()
|
// ...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 { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Metainfo from '../next/Metainfo.vue'
|
import { createManager, useMeta, useMetainfo } from '../../src'
|
||||||
import { createMeta, useMeta } from '../next'
|
|
||||||
// import About from './about.vue'
|
// import About from './about.vue'
|
||||||
|
|
||||||
const metaUpdated = 'no'
|
const metaUpdated = 'no'
|
||||||
@@ -9,47 +17,48 @@ const metaUpdated = 'no'
|
|||||||
const ChildComponent = defineComponent({
|
const ChildComponent = defineComponent({
|
||||||
name: 'child-component',
|
name: 'child-component',
|
||||||
props: {
|
props: {
|
||||||
page: String
|
page: String,
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
|
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
|
||||||
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
|
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
|
||||||
</div>`,
|
</div>`,
|
||||||
setup (props) {
|
setup(props) {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
date: null,
|
date: null,
|
||||||
metaUpdated
|
metaUpdated,
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = props.page[0].toUpperCase() + props.page.slice(1)
|
const title = props.page[0].toUpperCase() + props.page.slice(1)
|
||||||
|
console.log('ChildComponent Setup')
|
||||||
useMeta({
|
useMeta({
|
||||||
charset: 'utf16',
|
charset: 'utf16',
|
||||||
title,
|
title,
|
||||||
description: 'Description ' + props.page,
|
description: 'Description ' + props.page,
|
||||||
og: {
|
og: {
|
||||||
title: 'Og Title ' + props.page
|
title: 'Og Title ' + props.page,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...toRefs(state)
|
...toRefs(state),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function view (page) {
|
function view(page) {
|
||||||
return {
|
return {
|
||||||
name: `section-${page}`,
|
name: `section-${page}`,
|
||||||
render () {
|
render() {
|
||||||
return h(ChildComponent, { page })
|
return h(ChildComponent, { page })
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = {
|
const App = {
|
||||||
setup () {
|
setup() {
|
||||||
|
// console.log('App', getCurrentInstance())
|
||||||
const { meta } = useMeta({
|
const { meta } = useMeta({
|
||||||
base: { href: '/vue-router', target: '_blank' },
|
base: { href: '/vue-router', target: '_blank' },
|
||||||
charset: 'utf8',
|
charset: 'utf8',
|
||||||
@@ -60,23 +69,23 @@ const App = {
|
|||||||
description: 'Bla bla',
|
description: 'Bla bla',
|
||||||
image: [
|
image: [
|
||||||
'https://picsum.photos/600/400/?image=80',
|
'https://picsum.photos/600/400/?image=80',
|
||||||
'https://picsum.photos/600/400/?image=82'
|
'https://picsum.photos/600/400/?image=82',
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Twitter Title'
|
title: 'Twitter Title',
|
||||||
},
|
},
|
||||||
noscript: [
|
noscript: [
|
||||||
'<!-- // A code comment -->',
|
'<!-- // A code comment -->',
|
||||||
{ tag: 'link', rel: 'stylesheet', href: 'style.css' }
|
{ tag: 'link', rel: 'stylesheet', href: 'style.css' },
|
||||||
],
|
],
|
||||||
otherNoscript: {
|
otherNoscript: {
|
||||||
tag: 'noscript',
|
tag: 'noscript',
|
||||||
'data-test': 'hello',
|
'data-test': 'hello',
|
||||||
content: [
|
content: [
|
||||||
'<!-- // Another code comment -->',
|
'<!-- // Another code comment -->',
|
||||||
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' }
|
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
body: 'body-script1.js',
|
body: 'body-script1.js',
|
||||||
script: [
|
script: [
|
||||||
@@ -84,26 +93,32 @@ const App = {
|
|||||||
{ src: 'head-script1.js' },
|
{ src: 'head-script1.js' },
|
||||||
'<![endif]-->',
|
'<![endif]-->',
|
||||||
{ src: 'body-script2.js', target: 'body' },
|
{ src: 'body-script2.js', target: 'body' },
|
||||||
{ src: 'body-script3.js', target: '#put-it-here' }
|
{ src: 'body-script3.js', target: '#put-it-here' },
|
||||||
],
|
],
|
||||||
esi: {
|
esi: {
|
||||||
content: [{
|
content: [
|
||||||
tag: 'choose',
|
{
|
||||||
content: [{
|
tag: 'choose',
|
||||||
tag: 'when',
|
content: [
|
||||||
test: '$(HTTP_COOKIE{group})=="Advanced"',
|
{
|
||||||
content: [{
|
tag: 'when',
|
||||||
tag: 'include',
|
test: '$(HTTP_COOKIE{group})=="Advanced"',
|
||||||
src: 'http://www.example.com/advanced.html'
|
content: [
|
||||||
}]
|
{
|
||||||
}]
|
tag: 'include',
|
||||||
}]
|
src: 'http://www.example.com/advanced.html',
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => (meta.title = 'My Updated Title'), 2000)
|
setTimeout(() => (meta.title = 'My Updated Title'), 2000)
|
||||||
|
|
||||||
const metainfo = inject('metainfo')
|
const metainfo = useMetainfo()
|
||||||
|
|
||||||
window.$metainfo = metainfo
|
window.$metainfo = metainfo
|
||||||
|
|
||||||
@@ -112,7 +127,7 @@ const App = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metainfo
|
metainfo,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
@@ -133,56 +148,60 @@ const App = {
|
|||||||
</transition>
|
</transition>
|
||||||
<p>Inspect Element to see the meta info</p>
|
<p>Inspect Element to see the meta info</p>
|
||||||
</div>
|
</div>
|
||||||
`
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
function decisionMaker5000000 (key, options, currentValue) {
|
function decisionMaker5000000(key, pathSegments, getOptions, getCurrentValue) {
|
||||||
let theChosenOne
|
let theChosenOne
|
||||||
|
|
||||||
|
const options = getOptions()
|
||||||
|
|
||||||
for (const option of options) {
|
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
|
theChosenOne = option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(key, currentValue, options.map(({ value }) => value))
|
console.log(
|
||||||
|
key,
|
||||||
|
getCurrentValue(),
|
||||||
|
options.map(({ value }) => value)
|
||||||
|
)
|
||||||
console.log(theChosenOne.value)
|
console.log(theChosenOne.value)
|
||||||
return theChosenOne.value
|
return theChosenOne.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = createMeta({
|
const metaManager = createManager({
|
||||||
resolver: decisionMaker5000000,
|
resolver: decisionMaker5000000,
|
||||||
config: {
|
config: {
|
||||||
esi: {
|
esi: {
|
||||||
group: true,
|
group: true,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
contentAttributes: [
|
contentAttributes: ['src', 'test', 'text'],
|
||||||
'src',
|
},
|
||||||
'test',
|
},
|
||||||
'text'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useMeta(
|
||||||
|
{
|
||||||
|
og: {
|
||||||
|
something: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metaManager
|
||||||
|
)
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory('/vue-router'),
|
history: createWebHistory('/vue-router'),
|
||||||
routes: [
|
routes: [
|
||||||
{ name: 'home', path: '/', component: view('home') },
|
{ name: 'home', path: '/', component: view('home') },
|
||||||
{ name: 'about', path: '/about', component: view('about') }
|
{ name: 'about', path: '/about', component: view('about') },
|
||||||
]
|
],
|
||||||
})
|
|
||||||
|
|
||||||
useMeta({
|
|
||||||
og: {
|
|
||||||
something: 'test'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.component('metainfo', Metainfo)
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(meta)
|
app.use(metaManager)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
// old stuff:
|
// old stuff:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="/global.css">
|
<link rel="stylesheet" href="/_static/global.css">
|
||||||
<style>
|
<style>
|
||||||
.page-enter-active, .page-leave-active {
|
.page-enter-active, .page-leave-active {
|
||||||
transition: opacity .5s
|
transition: opacity .5s
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export default new Router({
|
|||||||
base: '/vuex-async',
|
base: '/vuex-async',
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: Home },
|
{ path: '/', component: Home },
|
||||||
{ path: '/posts/:slug', component: Post }
|
{ path: '/posts/:slug', component: Post },
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,60 +14,64 @@ export default new Vuex.Store({
|
|||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
published: false
|
published: false,
|
||||||
},
|
},
|
||||||
posts: [{
|
posts: [
|
||||||
slug: 'a-sample-blog-post',
|
{
|
||||||
title: 'A Sample Blog Post',
|
slug: 'a-sample-blog-post',
|
||||||
content: 'This is the blog post content',
|
title: 'A Sample Blog Post',
|
||||||
published: true
|
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',
|
slug: 'an-unpublished-blog-post',
|
||||||
published: false
|
title: 'An Unpublished Blog Post',
|
||||||
}, {
|
content: 'This is the blog post content',
|
||||||
slug: 'another-blog-post',
|
published: false,
|
||||||
title: 'Another Blog Post',
|
},
|
||||||
content: 'This is the blog post content',
|
{
|
||||||
published: true
|
slug: 'another-blog-post',
|
||||||
}]
|
title: 'Another Blog Post',
|
||||||
|
content: 'This is the blog post content',
|
||||||
|
published: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// GETTERS
|
// GETTERS
|
||||||
getters: {
|
getters: {
|
||||||
isLoading (state) {
|
isLoading(state) {
|
||||||
return state.isLoading
|
return state.isLoading
|
||||||
},
|
},
|
||||||
post (state) {
|
post(state) {
|
||||||
return state.post
|
return state.post
|
||||||
},
|
},
|
||||||
publishedPosts (state) {
|
publishedPosts(state) {
|
||||||
return state.posts.filter(post => post.published)
|
return state.posts.filter(post => post.published)
|
||||||
},
|
},
|
||||||
publishedPostsCount (state, getters) {
|
publishedPostsCount(state, getters) {
|
||||||
return getters.publishedPosts.length
|
return getters.publishedPosts.length
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// MUTATIONS
|
// MUTATIONS
|
||||||
mutations: {
|
mutations: {
|
||||||
loadingState (state, { isLoading }) {
|
loadingState(state, { isLoading }) {
|
||||||
state.isLoading = isLoading
|
state.isLoading = isLoading
|
||||||
},
|
},
|
||||||
getPost (state, { slug }) {
|
getPost(state, { slug }) {
|
||||||
state.post = state.posts.find(post => post.slug === slug)
|
state.post = state.posts.find(post => post.slug === slug)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ACTIONS
|
// ACTIONS
|
||||||
actions: {
|
actions: {
|
||||||
getPost ({ commit }, payload) {
|
getPost({ commit }, payload) {
|
||||||
commit('loadingState', { isLoading: true })
|
commit('loadingState', { isLoading: true })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
commit('getPost', payload)
|
commit('getPost', payload)
|
||||||
commit('loadingState', { isLoading: false })
|
commit('loadingState', { isLoading: false })
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export default new Router({
|
|||||||
base: '/vuex',
|
base: '/vuex',
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: Home },
|
{ 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: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
published: false
|
published: false,
|
||||||
},
|
},
|
||||||
posts: [{
|
posts: [
|
||||||
slug: 'a-sample-blog-post',
|
{
|
||||||
title: 'A Sample Blog Post',
|
slug: 'a-sample-blog-post',
|
||||||
content: 'This is the blog post content',
|
title: 'A Sample Blog Post',
|
||||||
published: true
|
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',
|
slug: 'an-unpublished-blog-post',
|
||||||
published: false
|
title: 'An Unpublished Blog Post',
|
||||||
}, {
|
content: 'This is the blog post content',
|
||||||
slug: 'another-blog-post',
|
published: false,
|
||||||
title: 'Another Blog Post',
|
},
|
||||||
content: 'This is the blog post content',
|
{
|
||||||
published: true
|
slug: 'another-blog-post',
|
||||||
}]
|
title: 'Another Blog Post',
|
||||||
|
content: 'This is the blog post content',
|
||||||
|
published: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// GETTERS
|
// GETTERS
|
||||||
getters: {
|
getters: {
|
||||||
post (state) {
|
post(state) {
|
||||||
return state.post
|
return state.post
|
||||||
},
|
},
|
||||||
publishedPosts (state) {
|
publishedPosts(state) {
|
||||||
return state.posts.filter(post => post.published)
|
return state.posts.filter(post => post.published)
|
||||||
},
|
},
|
||||||
publishedPostsCount (state, getters) {
|
publishedPostsCount(state, getters) {
|
||||||
return getters.publishedPosts.length
|
return getters.publishedPosts.length
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// MUTATIONS
|
// MUTATIONS
|
||||||
mutations: {
|
mutations: {
|
||||||
getPost (state, { slug }) {
|
getPost(state, { slug }) {
|
||||||
state.post = state.posts.find(post => post.slug === slug)
|
state.post = state.posts.find(post => post.slug === slug)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ACTIONS
|
// ACTIONS
|
||||||
actions: {
|
actions: {
|
||||||
getPost ({ commit }, payload) {
|
getPost({ commit }, payload) {
|
||||||
commit('getPost', 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",
|
"name": "vue-meta",
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"description": "Manage HTML metadata in Vue.js components with ssr support",
|
"description": "Manage HTML metadata in Vue.js components with SSR support",
|
||||||
"keywords": [
|
"main": "dist/vue-meta.common.js",
|
||||||
"attribute",
|
"web": "dist/vue-meta.js",
|
||||||
"google",
|
"module": "dist/vue-meta.esm.js",
|
||||||
"head",
|
"typings": "types/index.d.ts",
|
||||||
"helmet",
|
"files": [
|
||||||
"info",
|
"dist",
|
||||||
"metadata",
|
"types/*.d.ts"
|
||||||
"meta",
|
|
||||||
"seo",
|
|
||||||
"server",
|
|
||||||
"ssr",
|
|
||||||
"title",
|
|
||||||
"universal",
|
|
||||||
"vue"
|
|
||||||
],
|
],
|
||||||
"homepage": "https://github.com/nuxt/vue-meta",
|
"homepage": "https://github.com/nuxt/vue-meta",
|
||||||
"bugs": "https://github.com/nuxt/vue-meta/issues",
|
"bugs": "https://github.com/nuxt/vue-meta/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com/nuxt/vue-meta.git"
|
"url": "git+https://github.com/nuxt/vue-meta.git"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"contributors": [
|
"keywords": [
|
||||||
{
|
"google",
|
||||||
"name": "Declan de Wet (@declandewet)"
|
"head",
|
||||||
},
|
"metadata",
|
||||||
{
|
"meta",
|
||||||
"name": "Sebastien Chopin (@Atinux)"
|
"seo",
|
||||||
},
|
"ssr",
|
||||||
{
|
"title",
|
||||||
"name": "Pim (@pimlie)"
|
"universal",
|
||||||
}
|
"vue"
|
||||||
],
|
],
|
||||||
"files": [
|
"author": "Pim (@pimlie)",
|
||||||
"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",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rimraf dist && rollup -c scripts/rollup.config.js",
|
"build": "rimraf dist && rollup -c scripts/rollup.config.js",
|
||||||
"coverage": "codecov",
|
"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": "vuepress dev --host 0.0.0.0 --port 3000 docs",
|
||||||
"docs:build": "vuepress build 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",
|
"prerelease": "git checkout master && git pull -r",
|
||||||
"release": "yarn lint && yarn test && standard-version",
|
"release": "yarn lint && yarn test && standard-version",
|
||||||
"test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser",
|
"test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser",
|
||||||
@@ -58,69 +44,52 @@
|
|||||||
"test:unit": "jest test/unit",
|
"test:unit": "jest test/unit",
|
||||||
"test:types": "tsc -p types/test"
|
"test:types": "tsc -p types/test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"pperDependencies": {
|
||||||
"deepmerge": "^4.2.2"
|
"vue": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.8.4",
|
"@types/webpack": "^4.41.16",
|
||||||
"@babel/core": "^7.9.0",
|
"@types/webpack-env": "^1.15.2",
|
||||||
"@babel/node": "^7.8.7",
|
"@vue/compiler-sfc": "^3.0.0-beta.14",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@vue/server-test-utils": "^1.0.3",
|
||||||
"@babel/preset-env": "^7.9.0",
|
"@vue/test-utils": "^1.0.3",
|
||||||
"@nuxtjs/eslint-config": "^2.0.2",
|
"@wishy-gift/html-include-chunks-webpack-plugin": "^0.1.5",
|
||||||
"@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",
|
|
||||||
"browserstack-local": "^1.4.5",
|
"browserstack-local": "^1.4.5",
|
||||||
"chromedriver": "^80.0.1",
|
"chromedriver": "^83.0.0",
|
||||||
"codecov": "^3.6.5",
|
"codecov": "^3.7.0",
|
||||||
"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",
|
|
||||||
"geckodriver": "^1.19.1",
|
"geckodriver": "^1.19.1",
|
||||||
"get-port": "^5.1.1",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
"hable": "3.0.0",
|
"jest": "^26.0.1",
|
||||||
"is-wsl": "^2.1.1",
|
"jest-environment-jsdom": "^26.0.1",
|
||||||
"jest": "^25.2.3",
|
"jest-environment-jsdom-global": "^2.0.2",
|
||||||
"jest-environment-jsdom": "^25.2.3",
|
"jsdom": "^16.2.2",
|
||||||
"jest-environment-jsdom-global": "^1.2.1",
|
|
||||||
"jsdom": "^16.2.1",
|
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"node-env-file": "^0.1.8",
|
"node-env-file": "^0.1.8",
|
||||||
"puppeteer-core": "^2.1.1",
|
"prettier": "^2.0.5",
|
||||||
|
"puppeteer-core": "^3.2.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^2.2.0",
|
"rollup": "^2.11.2",
|
||||||
"rollup-plugin-babel": "^4.4.0",
|
"rollup-plugin-babel": "^4.4.0",
|
||||||
"rollup-plugin-commonjs": "^10.1.0",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
"rollup-plugin-json": "^4.0.0",
|
"rollup-plugin-json": "^4.0.0",
|
||||||
"rollup-plugin-node-resolve": "^5.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-replace": "^2.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",
|
"selenium-webdriver": "^4.0.0-alpha.5",
|
||||||
"standard-version": "^7.1.0",
|
"standard-version": "^8.0.0",
|
||||||
"tib": "^0.7.4",
|
"tib": "^0.7.4",
|
||||||
"typescript": "^3.8.3",
|
"ts-jest": "^26.1.0",
|
||||||
"vue": "^3.0.0-alpha.10",
|
"ts-loader": "^7.0.5",
|
||||||
|
"ts-node": "^8.10.2",
|
||||||
|
"typescript": "^3.9.3",
|
||||||
|
"vue": "next",
|
||||||
"vue-jest": "^3.0.5",
|
"vue-jest": "^3.0.5",
|
||||||
"vue-loader": "^15.9.1",
|
"vue-loader": "^16.0.0-beta.2",
|
||||||
"vue-router": "^3.1.6",
|
"vue-router": "next",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"webpack": "^4.43.0",
|
||||||
"vuepress": "^1.4.0",
|
"webpack-bundle-analyzer": "^3.8.0",
|
||||||
"vuepress-theme-vue": "^1.1.0",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack": "^4.42.1"
|
"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/')
|
const folder = path.resolve(__dirname, '..', 'fixtures/basic/.vue-meta/')
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
if (browserString.includes('browserstack') && browserString.includes('local')) {
|
if (
|
||||||
|
browserString.includes('browserstack') &&
|
||||||
|
browserString.includes('local')
|
||||||
|
) {
|
||||||
const envFile = path.resolve(__dirname, '..', '..', '.env-browserstack')
|
const envFile = path.resolve(__dirname, '..', '..', '.env-browserstack')
|
||||||
if (fs.existsSync(envFile)) {
|
if (fs.existsSync(envFile)) {
|
||||||
env(envFile)
|
env(envFile)
|
||||||
@@ -24,48 +27,52 @@ describe(browserString, () => {
|
|||||||
|
|
||||||
const port = await getPort()
|
const port = await getPort()
|
||||||
|
|
||||||
browser = await createBrowser(browserString, {
|
browser = await createBrowser(
|
||||||
folder,
|
browserString,
|
||||||
staticServer: {
|
{
|
||||||
folder,
|
folder,
|
||||||
port
|
staticServer: {
|
||||||
},
|
folder,
|
||||||
/* BrowserStackLocal: {
|
port,
|
||||||
|
},
|
||||||
|
/* BrowserStackLocal: {
|
||||||
localIdentifier: Math.round(99999 * Math.random())
|
localIdentifier: Math.round(99999 * Math.random())
|
||||||
}, */
|
}, */
|
||||||
extendPage (page) {
|
extendPage(page) {
|
||||||
return {
|
return {
|
||||||
async navigate (path) {
|
async navigate(path) {
|
||||||
await page.runAsyncScript((path) => {
|
await page.runAsyncScript(path => {
|
||||||
return new Promise((resolve) => {
|
return new Promise(resolve => {
|
||||||
const oldTitle = document.title
|
const oldTitle = document.title
|
||||||
|
|
||||||
// local firefox has sometimes not updated the title
|
// local firefox has sometimes not updated the title
|
||||||
// even when the DOM is supposed to be fully updated
|
// even when the DOM is supposed to be fully updated
|
||||||
const waitTitleChanged = function () {
|
const waitTitleChanged = function () {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
if (oldTitle !== document.title) {
|
if (oldTitle !== document.title) {
|
||||||
resolve()
|
resolve()
|
||||||
} else {
|
} else {
|
||||||
waitTitleChanged()
|
waitTitleChanged()
|
||||||
}
|
}
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.$vueMeta.$once('routeChanged', waitTitleChanged)
|
window.$vueMeta.$once('routeChanged', waitTitleChanged)
|
||||||
window.$vueMeta.$router.push(path)
|
window.$vueMeta.$router.push(path)
|
||||||
})
|
})
|
||||||
}, path)
|
}, path)
|
||||||
},
|
},
|
||||||
routeData () {
|
routeData() {
|
||||||
return page.runScript(() => ({
|
return page.runScript(() => ({
|
||||||
path: window.$vueMeta.$route.path,
|
path: window.$vueMeta.$route.path,
|
||||||
query: window.$vueMeta.$route.query
|
query: window.$vueMeta.$route.query,
|
||||||
}))
|
}))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}, false)
|
false
|
||||||
|
)
|
||||||
|
|
||||||
browser.addCapability('browserstack.console', 'info')
|
browser.addCapability('browserstack.console', 'info')
|
||||||
browser.addCapability('browserstack.networkLogs', 'true')
|
browser.addCapability('browserstack.networkLogs', 'true')
|
||||||
@@ -86,7 +93,9 @@ describe(browserString, () => {
|
|||||||
|
|
||||||
page = await browser.page(url)
|
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', 'lang')).toBe('en')
|
||||||
expect(await page.getAttribute('html', 'amp')).toBe('')
|
expect(await page.getAttribute('html', 'amp')).toBe('')
|
||||||
expect(await page.getAttribute('html', 'allowfullscreen')).toBe(null)
|
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:first-child')).toBe(1)
|
||||||
expect(await page.getElementCount('body noscript:last-child')).toBe(1)
|
expect(await page.getElementCount('body noscript:last-child')).toBe(1)
|
||||||
|
|
||||||
expect(await page.runScript(() => {
|
expect(
|
||||||
return window.loadTest
|
await page.runScript(() => {
|
||||||
})).toBe('loaded')
|
return window.loadTest
|
||||||
|
})
|
||||||
|
).toBe('loaded')
|
||||||
|
|
||||||
expect(await page.runScript(() => {
|
expect(
|
||||||
return window.loadCallback
|
await page.runScript(() => {
|
||||||
})).toBe('yes')
|
return window.loadCallback
|
||||||
|
})
|
||||||
|
).toBe('yes')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('/about', async () => {
|
test('/about', async () => {
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ describe('basic browser with ssr page', () => {
|
|||||||
expect(htmlTag).toContain(' lang="en" ')
|
expect(htmlTag).toContain(' lang="en" ')
|
||||||
expect(htmlTag).toContain(' amp ')
|
expect(htmlTag).toContain(' amp ')
|
||||||
expect(htmlTag).not.toContain('allowfullscreen')
|
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)
|
||||||
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')
|
const Post = () => import('./views/about.vue')
|
||||||
|
|
||||||
export default function createRouter () {
|
export default function createRouter() {
|
||||||
return new Router({
|
return new Router({
|
||||||
mode: 'hash',
|
mode: 'hash',
|
||||||
base: '/',
|
base: '/',
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: Home },
|
{ 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 App from './App.vue'
|
||||||
import createRouter from './router'
|
import createRouter from './router'
|
||||||
|
|
||||||
export default async function createServerApp () {
|
export default async function createServerApp() {
|
||||||
const VueMeta = await _import(getVueMetaPath())
|
const VueMeta = await _import(getVueMetaPath())
|
||||||
|
|
||||||
Vue.use(VueMeta)
|
Vue.use(VueMeta)
|
||||||
|
|||||||
+134
-79
@@ -1,7 +1,13 @@
|
|||||||
import { getComponentMetaInfo } from '../../src/shared/getComponentOption'
|
import { getComponentMetaInfo } from '../../src/shared/getComponentOption'
|
||||||
import _getMetaInfo from '../../src/shared/getMetaInfo'
|
import _getMetaInfo from '../../src/shared/getMetaInfo'
|
||||||
import { triggerUpdate, batchUpdate } from '../../src/client/update'
|
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 { defaultOptions } from '../../src/shared/constants'
|
||||||
|
|
||||||
import GoodbyeWorld from '../components/goodbye-world.vue'
|
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 KeepAlive from '../components/keep-alive.vue'
|
||||||
import Changed from '../components/changed.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/client/update')
|
||||||
jest.mock('../../src/utils/window', () => ({
|
jest.mock('../../src/utils/window', () => ({
|
||||||
hasGlobalWindow: false
|
hasGlobalWindow: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe('components', () => {
|
describe('components', () => {
|
||||||
@@ -30,21 +37,21 @@ describe('components', () => {
|
|||||||
elements = {
|
elements = {
|
||||||
html: document.createElement('html'),
|
html: document.createElement('html'),
|
||||||
head: document.createElement('head'),
|
head: document.createElement('head'),
|
||||||
body: document.createElement('body')
|
body: document.createElement('body'),
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.html.appendChild(elements.head)
|
elements.html.appendChild(elements.head)
|
||||||
elements.html.appendChild(elements.body)
|
elements.html.appendChild(elements.body)
|
||||||
|
|
||||||
document._getElementsByTagName = document.getElementsByTagName
|
document._getElementsByTagName = document.getElementsByTagName
|
||||||
jest.spyOn(document, 'getElementsByTagName').mockImplementation((tag) => {
|
jest.spyOn(document, 'getElementsByTagName').mockImplementation(tag => {
|
||||||
if (elements[tag]) {
|
if (elements[tag]) {
|
||||||
return [elements[tag]]
|
return [elements[tag]]
|
||||||
}
|
}
|
||||||
|
|
||||||
return document._getElementsByTagName(tag)
|
return document._getElementsByTagName(tag)
|
||||||
})
|
})
|
||||||
jest.spyOn(document, 'querySelectorAll').mockImplementation((query) => {
|
jest.spyOn(document, 'querySelectorAll').mockImplementation(query => {
|
||||||
return elements.html.querySelectorAll(query)
|
return elements.html.querySelectorAll(query)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -52,16 +59,22 @@ describe('components', () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks()
|
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.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.childNodes.forEach(child => child.remove())
|
||||||
elements.body.getAttributeNames().forEach(name => elements.body.removeAttribute(name))
|
elements.body
|
||||||
|
.getAttributeNames()
|
||||||
|
.forEach(name => elements.body.removeAttribute(name))
|
||||||
|
|
||||||
clearClientAttributeMap()
|
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 })
|
const wrapper = mount(HelloWorld, { localVue: Vue })
|
||||||
|
|
||||||
let metaInfo = getMetaInfo(wrapper.vm)
|
let metaInfo = getMetaInfo(wrapper.vm)
|
||||||
@@ -134,7 +147,9 @@ describe('components', () => {
|
|||||||
HelloWorld.metaInfo = metaInfo
|
HelloWorld.metaInfo = metaInfo
|
||||||
|
|
||||||
expect(warn).toHaveBeenCalledTimes(1)
|
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()
|
warn.mockRestore()
|
||||||
})
|
})
|
||||||
@@ -151,7 +166,7 @@ describe('components', () => {
|
|||||||
const { set } = this.$meta().addApp('inject-test-app')
|
const { set } = this.$meta().addApp('inject-test-app')
|
||||||
set({
|
set({
|
||||||
htmlAttrs: { lang: 'nl' },
|
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()
|
const metaInfo = wrapper.vm.$meta().inject()
|
||||||
expect(metaInfo.title.text()).toEqual('<title>Hello World</title>')
|
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.htmlAttrs.text()).toEqual(
|
||||||
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">')
|
'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
|
delete HelloWorld.created
|
||||||
})
|
})
|
||||||
@@ -170,15 +189,19 @@ describe('components', () => {
|
|||||||
HelloWorld.created = function () {
|
HelloWorld.created = function () {
|
||||||
const { set } = this.$meta().addApp('inject-test-app')
|
const { set } = this.$meta().addApp('inject-test-app')
|
||||||
set({
|
set({
|
||||||
meta: [{ skip: true, name: 'description', content: 'test-description' }],
|
meta: [
|
||||||
script: [{
|
{ skip: true, name: 'description', content: 'test-description' },
|
||||||
once: true,
|
],
|
||||||
callback: true,
|
script: [
|
||||||
async: false,
|
{
|
||||||
json: {
|
once: true,
|
||||||
a: 1
|
callback: true,
|
||||||
}
|
async: false,
|
||||||
}]
|
json: {
|
||||||
|
a: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,8 +209,12 @@ describe('components', () => {
|
|||||||
|
|
||||||
const metaInfo = wrapper.vm.$meta().inject()
|
const metaInfo = wrapper.vm.$meta().inject()
|
||||||
|
|
||||||
expect(metaInfo.meta.text()).toEqual('<meta data-vue-meta="ssr" charset="utf-8">')
|
expect(metaInfo.meta.text()).toEqual(
|
||||||
expect(metaInfo.script.text()).toEqual('<script onload="this.__vm_l=1">{"a":1}</script>')
|
'<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
|
delete HelloWorld.created
|
||||||
})
|
})
|
||||||
@@ -202,9 +229,9 @@ describe('components', () => {
|
|||||||
|
|
||||||
const Component = Vue.extend({
|
const Component = Vue.extend({
|
||||||
metaInfo: { title: 'Test' },
|
metaInfo: { title: 'Test' },
|
||||||
render (h) {
|
render(h) {
|
||||||
return h('div', null, 'Test')
|
return h('div', null, 'Test')
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const vm = new Component().$mount(el)
|
const vm = new Component().$mount(el)
|
||||||
@@ -249,7 +276,7 @@ describe('components', () => {
|
|||||||
const afterNavigation = jest.fn()
|
const afterNavigation = jest.fn()
|
||||||
const component = Vue.component('nav-component', {
|
const component = Vue.component('nav-component', {
|
||||||
render: h => h('div'),
|
render: h => h('div'),
|
||||||
metaInfo: { afterNavigation }
|
metaInfo: { afterNavigation },
|
||||||
})
|
})
|
||||||
|
|
||||||
const guards = {}
|
const guards = {}
|
||||||
@@ -257,14 +284,14 @@ describe('components', () => {
|
|||||||
localVue: Vue,
|
localVue: Vue,
|
||||||
mocks: {
|
mocks: {
|
||||||
$router: {
|
$router: {
|
||||||
beforeEach (fn) {
|
beforeEach(fn) {
|
||||||
guards.before = fn
|
guards.before = fn
|
||||||
},
|
},
|
||||||
afterEach (fn) {
|
afterEach(fn) {
|
||||||
guards.after = fn
|
guards.after = fn
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await vmTick(wrapper.vm)
|
await vmTick(wrapper.vm)
|
||||||
@@ -286,7 +313,7 @@ describe('components', () => {
|
|||||||
const afterNavigation = jest.fn()
|
const afterNavigation = jest.fn()
|
||||||
const component = Vue.component('nav-component', {
|
const component = Vue.component('nav-component', {
|
||||||
render: h => h('div'),
|
render: h => h('div'),
|
||||||
metaInfo: { afterNavigation }
|
metaInfo: { afterNavigation },
|
||||||
})
|
})
|
||||||
|
|
||||||
const guards = {}
|
const guards = {}
|
||||||
@@ -294,14 +321,14 @@ describe('components', () => {
|
|||||||
localVue: Vue,
|
localVue: Vue,
|
||||||
mocks: {
|
mocks: {
|
||||||
$router: {
|
$router: {
|
||||||
beforeEach (fn) {
|
beforeEach(fn) {
|
||||||
guards.before = fn
|
guards.before = fn
|
||||||
},
|
},
|
||||||
afterEach (fn) {
|
afterEach(fn) {
|
||||||
guards.after = fn
|
guards.after = fn
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await vmTick(wrapper.vm)
|
await vmTick(wrapper.vm)
|
||||||
@@ -334,27 +361,27 @@ describe('components', () => {
|
|||||||
// this component uses a computed prop to simulate a non-synchronous
|
// this component uses a computed prop to simulate a non-synchronous
|
||||||
// metaInfo update like you would have with a Vuex mutation
|
// metaInfo update like you would have with a Vuex mutation
|
||||||
const Component = Vue.extend({
|
const Component = Vue.extend({
|
||||||
metaInfo () {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
theme: this.theme
|
theme: this.theme,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
hiddenTheme: 'light'
|
hiddenTheme: 'light',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
theme () {
|
theme() {
|
||||||
return this.hiddenTheme
|
return this.hiddenTheme
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
beforeMount () {
|
beforeMount() {
|
||||||
this.hiddenTheme = 'dark'
|
this.hiddenTheme = 'dark'
|
||||||
},
|
},
|
||||||
render: h => h('div')
|
render: h => h('div'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const vm = new Component().$mount(el)
|
const vm = new Component().$mount(el)
|
||||||
@@ -385,27 +412,27 @@ describe('components', () => {
|
|||||||
document.body.appendChild(el)
|
document.body.appendChild(el)
|
||||||
|
|
||||||
const Component = Vue.extend({
|
const Component = Vue.extend({
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
hiddenTheme: 'light'
|
hiddenTheme: 'light',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
theme () {
|
theme() {
|
||||||
return this.hiddenTheme
|
return this.hiddenTheme
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted() {
|
||||||
this.hiddenTheme = 'dark'
|
this.hiddenTheme = 'dark'
|
||||||
},
|
},
|
||||||
render: h => h('div'),
|
render: h => h('div'),
|
||||||
metaInfo () {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
theme: this.theme
|
theme: this.theme,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const vm = new Component().$mount(el)
|
const vm = new Component().$mount(el)
|
||||||
@@ -431,7 +458,7 @@ describe('components', () => {
|
|||||||
// are really removed
|
// are really removed
|
||||||
const { set, remove } = this.$meta().addApp('my-bogus-app')
|
const { set, remove } = this.$meta().addApp('my-bogus-app')
|
||||||
set({
|
set({
|
||||||
meta: [{ name: 'og:description', content: 'test-description' }]
|
meta: [{ name: 'og:description', content: 'test-description' }],
|
||||||
})
|
})
|
||||||
remove()
|
remove()
|
||||||
|
|
||||||
@@ -439,12 +466,12 @@ describe('components', () => {
|
|||||||
app.set({
|
app.set({
|
||||||
htmlAttrs: { lang: 'nl' },
|
htmlAttrs: { lang: 'nl' },
|
||||||
meta: [{ name: 'description', content: 'test-description' }],
|
meta: [{ name: 'description', content: 'test-description' }],
|
||||||
script: [{ innerHTML: 'var test = true;' }]
|
script: [{ innerHTML: 'var test = true;' }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = mount(HelloWorld, {
|
const wrapper = mount(HelloWorld, {
|
||||||
localVue: Vue
|
localVue: Vue,
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.$meta().refresh()
|
wrapper.vm.$meta().refresh()
|
||||||
@@ -452,21 +479,28 @@ describe('components', () => {
|
|||||||
expect(html.getAttribute('lang')).toEqual('en nl')
|
expect(html.getAttribute('lang')).toEqual('en nl')
|
||||||
expect(Array.from(html.querySelectorAll('meta')).length).toBe(2)
|
expect(Array.from(html.querySelectorAll('meta')).length).toBe(2)
|
||||||
expect(Array.from(html.querySelectorAll('script')).length).toBe(1)
|
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()
|
app.remove()
|
||||||
|
|
||||||
// add another app to make sure on client data is immediately added
|
// add another app to make sure on client data is immediately added
|
||||||
const anotherApp = wrapper.vm.$meta().addApp('another-test-app')
|
const anotherApp = wrapper.vm.$meta().addApp('another-test-app')
|
||||||
anotherApp.set({
|
anotherApp.set({
|
||||||
meta: [{ name: 'og:description', content: 'test-description' }]
|
meta: [{ name: 'og:description', content: 'test-description' }],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(html.getAttribute('lang')).toEqual('en')
|
expect(html.getAttribute('lang')).toEqual('en')
|
||||||
expect(Array.from(html.querySelectorAll('meta')).length).toBe(2)
|
expect(Array.from(html.querySelectorAll('meta')).length).toBe(2)
|
||||||
expect(Array.from(html.querySelectorAll('script')).length).toBe(0)
|
expect(Array.from(html.querySelectorAll('script')).length).toBe(0)
|
||||||
expect(Array.from(html.querySelectorAll('[data-vue-meta="my-test-app"]')).length).toBe(0)
|
expect(
|
||||||
expect(Array.from(html.querySelectorAll('[data-vue-meta="another-test-app"]')).length).toBe(1)
|
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()
|
wrapper.destroy()
|
||||||
delete HelloWorld.created
|
delete HelloWorld.created
|
||||||
@@ -477,7 +511,10 @@ describe('components', () => {
|
|||||||
html.setAttribute(defaultOptions.ssrAttribute, 'true')
|
html.setAttribute(defaultOptions.ssrAttribute, 'true')
|
||||||
|
|
||||||
body.setAttribute('foo', 'bar')
|
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')
|
const el = document.createElement('div')
|
||||||
el.setAttribute('id', 'app')
|
el.setAttribute('id', 'app')
|
||||||
@@ -488,10 +525,10 @@ describe('components', () => {
|
|||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Test',
|
title: 'Test',
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
foo: 'bar'
|
foo: 'bar',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
render: h => h('div', null, 'Test')
|
render: h => h('div', null, 'Test'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const vm = new Component().$mount(el)
|
const vm = new Component().$mount(el)
|
||||||
@@ -500,7 +537,9 @@ describe('components', () => {
|
|||||||
|
|
||||||
wrapper.vm.$meta().refresh()
|
wrapper.vm.$meta().refresh()
|
||||||
expect(body.getAttribute('foo')).toBe('bar')
|
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()
|
wrapper.vm.$meta().refresh()
|
||||||
expect(body.getAttribute('foo')).toBe('bar')
|
expect(body.getAttribute('foo')).toBe('bar')
|
||||||
@@ -519,14 +558,14 @@ describe('components', () => {
|
|||||||
localVue: Vue,
|
localVue: Vue,
|
||||||
mocks: {
|
mocks: {
|
||||||
$router: {
|
$router: {
|
||||||
beforeEach (fn) {
|
beforeEach(fn) {
|
||||||
guards.before = fn
|
guards.before = fn
|
||||||
},
|
},
|
||||||
afterEach (fn) {
|
afterEach(fn) {
|
||||||
guards.after = fn
|
guards.after = fn
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(guards.before).toBeUndefined()
|
expect(guards.before).toBeUndefined()
|
||||||
@@ -540,8 +579,13 @@ describe('components', () => {
|
|||||||
|
|
||||||
test('destroyed hook calls triggerUpdate delayed', async () => {
|
test('destroyed hook calls triggerUpdate delayed', async () => {
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
const wrapper = mount(HelloWorld, { localVue: Vue, parentComponent: { render: h => h('div') } })
|
const wrapper = mount(HelloWorld, {
|
||||||
const spy = jest.spyOn(wrapper.vm.$el, 'offsetParent', 'get').mockReturnValue(true)
|
localVue: Vue,
|
||||||
|
parentComponent: { render: h => h('div') },
|
||||||
|
})
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(wrapper.vm.$el, 'offsetParent', 'get')
|
||||||
|
.mockReturnValue(true)
|
||||||
|
|
||||||
wrapper.destroy()
|
wrapper.destroy()
|
||||||
|
|
||||||
@@ -553,19 +597,30 @@ describe('components', () => {
|
|||||||
jest.advanceTimersByTime(51)
|
jest.advanceTimersByTime(51)
|
||||||
|
|
||||||
expect(triggerUpdate).toHaveBeenCalledTimes(2)
|
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 () => {
|
test('destroyed hook calls triggerUpdate immediately when waitOnDestroyed: false', async () => {
|
||||||
jest.useFakeTimers()
|
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.vm.$meta().setOptions({ waitOnDestroyed: false })
|
||||||
wrapper.destroy()
|
wrapper.destroy()
|
||||||
|
|
||||||
await vmTick(wrapper.vm)
|
await vmTick(wrapper.vm)
|
||||||
|
|
||||||
expect(triggerUpdate).toHaveBeenCalledTimes(2)
|
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 { defaultOptions } from '../../src/shared/constants'
|
||||||
import { serverSequences } from '../../src/shared/escaping'
|
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', () => {
|
describe('escaping', () => {
|
||||||
let Vue
|
let Vue
|
||||||
@@ -17,8 +22,8 @@ describe('escaping', () => {
|
|||||||
htmlAttrs: { key: 1 },
|
htmlAttrs: { key: 1 },
|
||||||
title: 'Hello & Goodbye',
|
title: 'Hello & Goodbye',
|
||||||
script: [{ innerHTML: 'Hello & Goodbye' }],
|
script: [{ innerHTML: 'Hello & Goodbye' }],
|
||||||
__dangerouslyDisableSanitizers: ['script']
|
__dangerouslyDisableSanitizers: ['script'],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
||||||
@@ -26,7 +31,7 @@ describe('escaping', () => {
|
|||||||
titleChunk: 'Hello & Goodbye',
|
titleChunk: 'Hello & Goodbye',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
key: 1
|
key: 1,
|
||||||
},
|
},
|
||||||
headAttrs: {},
|
headAttrs: {},
|
||||||
bodyAttrs: {},
|
bodyAttrs: {},
|
||||||
@@ -37,15 +42,15 @@ describe('escaping', () => {
|
|||||||
script: [{ innerHTML: 'Hello & Goodbye' }],
|
script: [{ innerHTML: 'Hello & Goodbye' }],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: ['script'],
|
__dangerouslyDisableSanitizers: ['script'],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('null title is left as it is', () => {
|
test('null title is left as it is', () => {
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: null
|
title: null,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
||||||
@@ -62,7 +67,7 @@ describe('escaping', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -72,10 +77,10 @@ describe('escaping', () => {
|
|||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
script: [
|
script: [
|
||||||
{ vmid: 'yescape', innerHTML: 'Hello & Goodbye' },
|
{ 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({
|
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
||||||
@@ -91,11 +96,11 @@ describe('escaping', () => {
|
|||||||
style: [],
|
style: [],
|
||||||
script: [
|
script: [
|
||||||
{ innerHTML: 'Hello & Goodbye', vmid: 'yescape' },
|
{ innerHTML: 'Hello & Goodbye', vmid: 'yescape' },
|
||||||
{ innerHTML: 'Hello & Goodbye', vmid: 'noscape' }
|
{ innerHTML: 'Hello & Goodbye', vmid: 'noscape' },
|
||||||
],
|
],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] }
|
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,12 +110,13 @@ describe('escaping', () => {
|
|||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
json: {
|
json: {
|
||||||
perfectlySave: '</script><p class="unsafe">This is safe</p><script>',
|
perfectlySave:
|
||||||
'</script>unsafeKey': 'This is also still safe'
|
'</script><p class="unsafe">This is safe</p><script>',
|
||||||
}
|
'</script>unsafeKey': 'This is also still safe',
|
||||||
}
|
},
|
||||||
]
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component, serverSequences)).toEqual({
|
expect(getMetaInfo(component, serverSequences)).toEqual({
|
||||||
@@ -127,14 +133,15 @@ describe('escaping', () => {
|
|||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
json: {
|
json: {
|
||||||
perfectlySave: '</script><p class="unsafe">This is safe</p><script>',
|
perfectlySave:
|
||||||
'</script>unsafeKey': 'This is also still safe'
|
'</script><p class="unsafe">This is safe</p><script>',
|
||||||
}
|
'</script>unsafeKey': 'This is also still safe',
|
||||||
}
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { defaultOptions } from '../../src/shared/constants'
|
|||||||
import metaInfoData from '../utils/meta-info-data'
|
import metaInfoData from '../utils/meta-info-data'
|
||||||
import { titleGenerator } from '../../src/server/generators'
|
import { titleGenerator } from '../../src/server/generators'
|
||||||
|
|
||||||
const generateServerInjector = metaInfo => _generateServerInjector(defaultOptions, metaInfo).injectors
|
const generateServerInjector = metaInfo =>
|
||||||
|
_generateServerInjector(defaultOptions, metaInfo).injectors
|
||||||
|
|
||||||
describe('generators', () => {
|
describe('generators', () => {
|
||||||
for (const type in metaInfoData) {
|
for (const type in metaInfoData) {
|
||||||
const typeTests = metaInfoData[type]
|
const typeTests = metaInfoData[type]
|
||||||
|
|
||||||
const testCases = {
|
const testCases = {
|
||||||
add: (tags) => {
|
add: tags => {
|
||||||
let html = tags.text()
|
let html = tags.text()
|
||||||
|
|
||||||
// ssr only returns the attributes, convert to full tag
|
// ssr only returns the attributes, convert to full tag
|
||||||
@@ -18,14 +19,14 @@ describe('generators', () => {
|
|||||||
html = `<${type.substr(0, 4)} ${html}>`
|
html = `<${type.substr(0, 4)} ${html}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
typeTests.add.expect.forEach((expected) => {
|
typeTests.add.expect.forEach(expected => {
|
||||||
expect(html).toContain(expected)
|
expect(html).toContain(expected)
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
describe(`${type} type tests`, () => {
|
describe(`${type} type tests`, () => {
|
||||||
Object.keys(typeTests).forEach((action) => {
|
Object.keys(typeTests).forEach(action => {
|
||||||
const testInfo = typeTests[action]
|
const testInfo = typeTests[action]
|
||||||
|
|
||||||
// return when no test case available
|
// return when no test case available
|
||||||
@@ -98,7 +99,9 @@ describe('extra tests', () => {
|
|||||||
|
|
||||||
expect(scriptTags.text()).toBe('')
|
expect(scriptTags.text()).toBe('')
|
||||||
expect(scriptTags.text({ body: true })).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', () => {
|
test('script append body', () => {
|
||||||
@@ -106,7 +109,9 @@ describe('extra tests', () => {
|
|||||||
const { script: scriptTags } = generateServerInjector({ script: tags })
|
const { script: scriptTags } = generateServerInjector({ script: tags })
|
||||||
|
|
||||||
expect(scriptTags.text()).toBe('')
|
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('')
|
expect(scriptTags.text({ pbody: true })).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -115,11 +120,11 @@ describe('extra tests', () => {
|
|||||||
title: 'hello',
|
title: 'hello',
|
||||||
htmlAttrs: { lang: 'en' },
|
htmlAttrs: { lang: 'en' },
|
||||||
bodyAttrs: { class: 'base-class' },
|
bodyAttrs: { class: 'base-class' },
|
||||||
script: [{ src: '/script.js', body: true }]
|
script: [{ src: '/script.js', body: true }],
|
||||||
}
|
}
|
||||||
const extraInfo = {
|
const extraInfo = {
|
||||||
bodyAttrs: { class: 'extra-class' },
|
bodyAttrs: { class: 'extra-class' },
|
||||||
script: [{ src: '/script.js', pbody: true }]
|
script: [{ src: '/script.js', pbody: true }],
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverInjector = _generateServerInjector(defaultOptions, baseInfo)
|
const serverInjector = _generateServerInjector(defaultOptions, baseInfo)
|
||||||
@@ -128,14 +133,26 @@ describe('extra tests', () => {
|
|||||||
const meta = serverInjector.injectors
|
const meta = serverInjector.injectors
|
||||||
|
|
||||||
expect(meta.script.text()).toBe('')
|
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({ body: true })).toBe(
|
||||||
expect(meta.script.text({ pbody: true })).toBe('<script data-vue-meta="test-app" src="/script.js" data-pbody="true"></script>')
|
'<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.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.bodyPrepend(true)).toBe(
|
||||||
expect(meta.bodyAppend()).toBe('<script data-vue-meta="ssr" src="/script.js" data-body="true"></script>')
|
'<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.htmlAttrs.text()).toBe(
|
||||||
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"')
|
'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', () => {
|
test('fetches the given option from the given component', () => {
|
||||||
const component = new Vue({ someOption: { foo: 'bar' } })
|
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).toBeDefined()
|
||||||
expect(mergedOption.foo).toEqual('bar')
|
expect(mergedOption.foo).toEqual('bar')
|
||||||
})
|
})
|
||||||
@@ -23,14 +26,14 @@ describe('getComponentOption', () => {
|
|||||||
test('calls a function as computed prop, injecting the component as context', () => {
|
test('calls a function as computed prop, injecting the component as context', () => {
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
name: 'Foobar',
|
name: 'Foobar',
|
||||||
someFunc () {
|
someFunc() {
|
||||||
return { opt: this.name }
|
return { opt: this.name }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
$metaInfo () {
|
$metaInfo() {
|
||||||
return this.$options.someFunc()
|
return this.$options.someFunc()
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const mergedOption = getComponentOption({ keyName: 'someFunc' }, component)
|
const mergedOption = getComponentOption({ keyName: 'someFunc' }, component)
|
||||||
@@ -41,11 +44,14 @@ describe('getComponentOption', () => {
|
|||||||
|
|
||||||
test('fetches deeply nested component options and merges them', () => {
|
test('fetches deeply nested component options and merges them', () => {
|
||||||
const localVue = loadVueMetaPlugin({ keyName: 'foo' })
|
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', {
|
const component = localVue.component('parent', {
|
||||||
foo: { fizz: 'buzz' },
|
foo: { fizz: 'buzz' },
|
||||||
render: h => h('div', null, [h('merge-child')])
|
render: h => h('div', null, [h('merge-child')]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const wrapper = mount(component, { localVue })
|
const wrapper = mount(component, { localVue })
|
||||||
@@ -96,23 +102,24 @@ describe('getComponentOption', () => {
|
|||||||
|
|
||||||
localVue.component('meta-child', {
|
localVue.component('meta-child', {
|
||||||
foo: { bar: 'baz' },
|
foo: { bar: 'baz' },
|
||||||
render (h) {
|
render(h) {
|
||||||
return h('div', this.$slots.default)
|
return h('div', this.$slots.default)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
localVue.component('nometa-child', {
|
localVue.component('nometa-child', {
|
||||||
render (h) {
|
render(h) {
|
||||||
return h('div', this.$slots.default)
|
return h('div', this.$slots.default)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = localVue.component('parent', {
|
const component = localVue.component('parent', {
|
||||||
render: h => h('div', null, [
|
render: h =>
|
||||||
h('meta-child', null, [h('nometa-child')]),
|
h('div', null, [
|
||||||
h('nometa-child', null, [h('meta-child')]),
|
h('meta-child', null, [h('nometa-child')]),
|
||||||
h('nometa-child')
|
h('nometa-child', null, [h('meta-child')]),
|
||||||
])
|
h('nometa-child'),
|
||||||
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const wrapper = mount(component, { localVue })
|
const wrapper = mount(component, { localVue })
|
||||||
@@ -136,22 +143,20 @@ describe('getComponentOption', () => {
|
|||||||
|
|
||||||
localVue.component('meta-child', {
|
localVue.component('meta-child', {
|
||||||
foo: { bar: 'baz' },
|
foo: { bar: 'baz' },
|
||||||
render (h) {
|
render(h) {
|
||||||
return h('div', this.$slots.default)
|
return h('div', this.$slots.default)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
localVue.component('nometa-child', {
|
localVue.component('nometa-child', {
|
||||||
render (h) {
|
render(h) {
|
||||||
return h('div', this.$slots.default)
|
return h('div', this.$slots.default)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = localVue.component('parent', {
|
const component = localVue.component('parent', {
|
||||||
foo: () => {},
|
foo: () => {},
|
||||||
render: h => h('div', null, [
|
render: h => h('div', null, [h('meta-child')]),
|
||||||
h('meta-child')
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const wrapper = mount(component, { localVue })
|
const wrapper = mount(component, { localVue })
|
||||||
|
|||||||
+158
-168
@@ -3,7 +3,13 @@ import _getMetaInfo from '../../src/shared/getMetaInfo'
|
|||||||
import { loadVueMetaPlugin } from '../utils'
|
import { loadVueMetaPlugin } from '../utils'
|
||||||
import { defaultOptions } from '../../src/shared/constants'
|
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', () => {
|
describe('getMetaInfo', () => {
|
||||||
let Vue
|
let Vue
|
||||||
@@ -27,7 +33,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -35,10 +41,8 @@ describe('getMetaInfo', () => {
|
|||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }],
|
||||||
{ charset: 'utf-8' }
|
},
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -48,16 +52,14 @@ describe('getMetaInfo', () => {
|
|||||||
htmlAttrs: {},
|
htmlAttrs: {},
|
||||||
headAttrs: {},
|
headAttrs: {},
|
||||||
bodyAttrs: {},
|
bodyAttrs: {},
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }],
|
||||||
{ charset: 'utf-8' }
|
|
||||||
],
|
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
style: [],
|
style: [],
|
||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -65,8 +67,8 @@ describe('getMetaInfo', () => {
|
|||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
base: { href: 'href' }
|
base: { href: 'href' },
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -77,15 +79,13 @@ describe('getMetaInfo', () => {
|
|||||||
headAttrs: {},
|
headAttrs: {},
|
||||||
bodyAttrs: {},
|
bodyAttrs: {},
|
||||||
meta: [],
|
meta: [],
|
||||||
base: [
|
base: [{ href: 'href' }],
|
||||||
{ href: 'href' }
|
|
||||||
],
|
|
||||||
link: [],
|
link: [],
|
||||||
style: [],
|
style: [],
|
||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -97,15 +97,15 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'a',
|
vmid: 'a',
|
||||||
property: 'a',
|
property: 'a',
|
||||||
content: 'a'
|
content: 'a',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
vmid: 'a',
|
vmid: 'a',
|
||||||
property: 'a',
|
property: 'a',
|
||||||
content: 'b'
|
content: 'b',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -119,8 +119,8 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'a',
|
vmid: 'a',
|
||||||
property: 'a',
|
property: 'a',
|
||||||
content: 'a'
|
content: 'a',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -128,7 +128,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -137,10 +137,8 @@ describe('getMetaInfo', () => {
|
|||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleTemplate: '%s World',
|
titleTemplate: '%s World',
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }],
|
||||||
{ charset: 'utf-8' }
|
},
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -150,16 +148,14 @@ describe('getMetaInfo', () => {
|
|||||||
htmlAttrs: {},
|
htmlAttrs: {},
|
||||||
headAttrs: {},
|
headAttrs: {},
|
||||||
bodyAttrs: {},
|
bodyAttrs: {},
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }],
|
||||||
{ charset: 'utf-8' }
|
|
||||||
],
|
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
style: [],
|
style: [],
|
||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -170,10 +166,8 @@ describe('getMetaInfo', () => {
|
|||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleTemplate,
|
titleTemplate,
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }],
|
||||||
{ charset: 'utf-8' }
|
},
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -183,16 +177,14 @@ describe('getMetaInfo', () => {
|
|||||||
htmlAttrs: {},
|
htmlAttrs: {},
|
||||||
headAttrs: {},
|
headAttrs: {},
|
||||||
bodyAttrs: {},
|
bodyAttrs: {},
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }],
|
||||||
{ charset: 'utf-8' }
|
|
||||||
],
|
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
style: [],
|
style: [],
|
||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -205,15 +197,13 @@ describe('getMetaInfo', () => {
|
|||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleTemplate,
|
titleTemplate,
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }],
|
||||||
{ charset: 'utf-8' }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
helloWorldText: 'Function World'
|
helloWorldText: 'Function World',
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -223,16 +213,14 @@ describe('getMetaInfo', () => {
|
|||||||
htmlAttrs: {},
|
htmlAttrs: {},
|
||||||
headAttrs: {},
|
headAttrs: {},
|
||||||
bodyAttrs: {},
|
bodyAttrs: {},
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }],
|
||||||
{ charset: 'utf-8' }
|
|
||||||
],
|
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
style: [],
|
style: [],
|
||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -245,10 +233,10 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: '%s - My page'
|
template: '%s - My page',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const expectedMetaInfo = {
|
const expectedMetaInfo = {
|
||||||
@@ -263,8 +251,8 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title - My page',
|
content: 'Test title - My page',
|
||||||
template: true
|
template: true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -272,7 +260,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||||
@@ -288,10 +276,10 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: chunk => `${chunk} - My page`
|
template: chunk => `${chunk} - My page`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const expectedMetaInfo = {
|
const expectedMetaInfo = {
|
||||||
@@ -306,8 +294,8 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title - My page',
|
content: 'Test title - My page',
|
||||||
template: true
|
template: true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -315,7 +303,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||||
@@ -330,10 +318,10 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title'
|
content: 'Test title',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -347,8 +335,8 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title'
|
content: 'Test title',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -356,7 +344,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -369,10 +357,10 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: null
|
template: null,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -386,8 +374,8 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title'
|
content: 'Test title',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -395,7 +383,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -408,10 +396,10 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: false
|
template: false,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -425,8 +413,8 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title'
|
content: 'Test title',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -434,7 +422,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -447,10 +435,10 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'An important title!'
|
content: 'An important title!',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
@@ -460,12 +448,12 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: chunk => `${chunk} - My page`
|
template: chunk => `${chunk} - My page`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
el: document.createElement('div'),
|
el: document.createElement('div'),
|
||||||
render: h => h('div', null, [h('merge-child')])
|
render: h => h('div', null, [h('merge-child')]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const expectedMetaInfo = {
|
const expectedMetaInfo = {
|
||||||
@@ -480,8 +468,8 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'An important title! - My page',
|
content: 'An important title! - My page',
|
||||||
template: true
|
template: true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -489,7 +477,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||||
@@ -505,10 +493,10 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
template: chunk => `${chunk} - My page`
|
template: chunk => `${chunk} - My page`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
@@ -518,12 +506,12 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: chunk => `${chunk} - SHOULD NEVER HAPPEN`
|
template: chunk => `${chunk} - SHOULD NEVER HAPPEN`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
el: document.createElement('div'),
|
el: document.createElement('div'),
|
||||||
render: h => h('div', null, [h('merge-child')])
|
render: h => h('div', null, [h('merge-child')]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const expectedMetaInfo = {
|
const expectedMetaInfo = {
|
||||||
@@ -538,8 +526,8 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title - My page',
|
content: 'Test title - My page',
|
||||||
template: true
|
template: true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -547,7 +535,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||||
@@ -564,10 +552,10 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'An important title!',
|
content: 'An important title!',
|
||||||
template: chunk => `${chunk} - My page`
|
template: chunk => `${chunk} - My page`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
@@ -577,12 +565,12 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: chunk => `${chunk} - SHOULD NEVER HAPPEN`
|
template: chunk => `${chunk} - SHOULD NEVER HAPPEN`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
el: document.createElement('div'),
|
el: document.createElement('div'),
|
||||||
render: h => h('div', null, [h('merge-child')])
|
render: h => h('div', null, [h('merge-child')]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const expectedMetaInfo = {
|
const expectedMetaInfo = {
|
||||||
@@ -597,8 +585,8 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'An important title! - My page',
|
content: 'An important title! - My page',
|
||||||
template: true
|
template: true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -606,7 +594,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||||
@@ -623,10 +611,10 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'An important title!',
|
content: 'An important title!',
|
||||||
template: chunk => `${chunk} - My page`
|
template: chunk => `${chunk} - My page`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
@@ -635,12 +623,12 @@ describe('getMetaInfo', () => {
|
|||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title'
|
content: 'Test title',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
el: document.createElement('div'),
|
el: document.createElement('div'),
|
||||||
render: h => h('div', null, [h('merge-child')])
|
render: h => h('div', null, [h('merge-child')]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const expectedMetaInfo = {
|
const expectedMetaInfo = {
|
||||||
@@ -655,8 +643,8 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'An important title! - My page',
|
content: 'An important title! - My page',
|
||||||
template: true
|
template: true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -664,7 +652,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
expect(getMetaInfo(component)).toEqual(expectedMetaInfo)
|
||||||
@@ -673,9 +661,9 @@ describe('getMetaInfo', () => {
|
|||||||
|
|
||||||
test('no errors when metaInfo returns nothing', () => {
|
test('no errors when metaInfo returns nothing', () => {
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo () {},
|
metaInfo() {},
|
||||||
el: document.createElement('div'),
|
el: document.createElement('div'),
|
||||||
render: h => h('div', null, [])
|
render: h => h('div', null, []),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -692,7 +680,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -702,34 +690,34 @@ describe('getMetaInfo', () => {
|
|||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: undefined,
|
title: undefined,
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: undefined
|
class: undefined,
|
||||||
},
|
},
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
content: undefined
|
content: undefined,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: 'class'
|
class: 'class',
|
||||||
},
|
},
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: chunk => `${chunk} - My page`
|
template: chunk => `${chunk} - My page`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
el: document.createElement('div'),
|
el: document.createElement('div'),
|
||||||
render: h => h('div', null, [h('merge-child')])
|
render: h => h('div', null, [h('merge-child')]),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -737,7 +725,7 @@ describe('getMetaInfo', () => {
|
|||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: 'class'
|
class: 'class',
|
||||||
},
|
},
|
||||||
headAttrs: {},
|
headAttrs: {},
|
||||||
htmlAttrs: {},
|
htmlAttrs: {},
|
||||||
@@ -746,8 +734,8 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title - My page',
|
content: 'Test title - My page',
|
||||||
template: true
|
template: true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
base: [],
|
base: [],
|
||||||
link: [],
|
link: [],
|
||||||
@@ -755,7 +743,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -767,10 +755,10 @@ describe('getMetaInfo', () => {
|
|||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
content: null
|
content: null,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
@@ -781,12 +769,12 @@ describe('getMetaInfo', () => {
|
|||||||
vmid: 'og:title',
|
vmid: 'og:title',
|
||||||
property: 'og:title',
|
property: 'og:title',
|
||||||
content: 'Test title',
|
content: 'Test title',
|
||||||
template: chunk => `${chunk} - My page`
|
template: chunk => `${chunk} - My page`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
el: document.createElement('div'),
|
el: document.createElement('div'),
|
||||||
render: h => h('div', null, [h('merge-child')])
|
render: h => h('div', null, [h('merge-child')]),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -803,7 +791,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -812,19 +800,19 @@ describe('getMetaInfo', () => {
|
|||||||
render: h => h('div'),
|
render: h => h('div'),
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: ['foo']
|
class: ['foo'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: ['bar']
|
class: ['bar'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
el: document.createElement('div'),
|
el: document.createElement('div'),
|
||||||
render: h => h('div', null, [h('merge-child')])
|
render: h => h('div', null, [h('merge-child')]),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -834,7 +822,7 @@ describe('getMetaInfo', () => {
|
|||||||
htmlAttrs: {},
|
htmlAttrs: {},
|
||||||
headAttrs: {},
|
headAttrs: {},
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: ['bar', 'foo']
|
class: ['bar', 'foo'],
|
||||||
},
|
},
|
||||||
meta: [],
|
meta: [],
|
||||||
base: [],
|
base: [],
|
||||||
@@ -843,7 +831,7 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -853,9 +841,9 @@ describe('getMetaInfo', () => {
|
|||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
amp: undefined
|
amp: undefined,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).toEqual({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
@@ -872,10 +860,12 @@ describe('getMetaInfo', () => {
|
|||||||
script: [],
|
script: [],
|
||||||
noscript: [],
|
noscript: [],
|
||||||
__dangerouslyDisableSanitizers: [],
|
__dangerouslyDisableSanitizers: [],
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(warn).toHaveBeenCalledTimes(1)
|
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 = {
|
const onLoadAttribute = {
|
||||||
k: 'onload',
|
k: 'onload',
|
||||||
v: 'this.__vm_l=1'
|
v: 'this.__vm_l=1',
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLoadAttribute = () => `${onLoadAttribute.k}="${onLoadAttribute.v}"`
|
const getLoadAttribute = () => `${onLoadAttribute.k}="${onLoadAttribute.v}"`
|
||||||
@@ -84,7 +84,9 @@ describe('load callbacks', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('addCallbacks', () => {
|
test('addCallbacks', () => {
|
||||||
const addListeners = jest.spyOn(document, 'querySelectorAll').mockReturnValue(false)
|
const addListeners = jest
|
||||||
|
.spyOn(document, 'querySelectorAll')
|
||||||
|
.mockReturnValue(false)
|
||||||
|
|
||||||
const config = { tagIDKeyName: 'test-id' }
|
const config = { tagIDKeyName: 'test-id' }
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ describe('load callbacks', () => {
|
|||||||
{ [config.tagIDKeyName]: 'test1', callback: false },
|
{ [config.tagIDKeyName]: 'test1', callback: false },
|
||||||
{ [config.tagIDKeyName]: false, callback: () => {} },
|
{ [config.tagIDKeyName]: false, callback: () => {} },
|
||||||
{ [config.tagIDKeyName]: 'test1', callback: () => {} },
|
{ [config.tagIDKeyName]: 'test1', callback: () => {} },
|
||||||
{ [config.tagIDKeyName]: 'test2', callback: () => {} }
|
{ [config.tagIDKeyName]: 'test2', callback: () => {} },
|
||||||
]
|
]
|
||||||
|
|
||||||
load.addCallbacks(config, 'link', tags)
|
load.addCallbacks(config, 'link', tags)
|
||||||
@@ -101,20 +103,27 @@ describe('load callbacks', () => {
|
|||||||
load.applyCallbacks({ matches })
|
load.applyCallbacks({ matches })
|
||||||
|
|
||||||
expect(matches).toHaveBeenCalledTimes(2)
|
expect(matches).toHaveBeenCalledTimes(2)
|
||||||
expect(matches).toHaveBeenCalledWith(`link[data-${config.tagIDKeyName}="test1"][${getLoadAttribute()}]`)
|
expect(matches).toHaveBeenCalledWith(
|
||||||
expect(matches).toHaveBeenCalledWith(`link[data-${config.tagIDKeyName}="test2"][${getLoadAttribute()}]`)
|
`link[data-${config.tagIDKeyName}="test1"][${getLoadAttribute()}]`
|
||||||
|
)
|
||||||
|
expect(matches).toHaveBeenCalledWith(
|
||||||
|
`link[data-${config.tagIDKeyName}="test2"][${getLoadAttribute()}]`
|
||||||
|
)
|
||||||
|
|
||||||
expect(addListeners).not.toHaveBeenCalled()
|
expect(addListeners).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('addCallbacks (auto add listeners)', () => {
|
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 = [
|
const tags = [{ [config.tagIDKeyName]: 'test1', callback: () => {} }]
|
||||||
{ [config.tagIDKeyName]: 'test1', callback: () => {} }
|
|
||||||
]
|
|
||||||
|
|
||||||
load.addCallbacks(config, 'style', tags, true)
|
load.addCallbacks(config, 'style', tags, true)
|
||||||
|
|
||||||
@@ -122,7 +131,9 @@ describe('load callbacks', () => {
|
|||||||
load.applyCallbacks({ matches })
|
load.applyCallbacks({ matches })
|
||||||
|
|
||||||
expect(matches).toHaveBeenCalledTimes(1)
|
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()
|
expect(addListeners).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -188,9 +199,11 @@ describe('load callbacks', () => {
|
|||||||
|
|
||||||
const el = document.createElement('script')
|
const el = document.createElement('script')
|
||||||
const addEventListener = el.addEventListener.bind(el)
|
const addEventListener = el.addEventListener.bind(el)
|
||||||
const addEventListenerSpy = jest.spyOn(el, 'addEventListener').mockImplementation((...args) => {
|
const addEventListenerSpy = jest
|
||||||
return addEventListener(...args)
|
.spyOn(el, 'addEventListener')
|
||||||
})
|
.mockImplementation((...args) => {
|
||||||
|
return addEventListener(...args)
|
||||||
|
})
|
||||||
el.setAttribute(onLoadAttribute.k, onLoadAttribute.v)
|
el.setAttribute(onLoadAttribute.k, onLoadAttribute.v)
|
||||||
document.body.appendChild(el)
|
document.body.appendChild(el)
|
||||||
|
|
||||||
|
|||||||
+43
-33
@@ -4,7 +4,7 @@ import { defaultOptions } from '../../src/shared/constants'
|
|||||||
|
|
||||||
jest.mock('../../src/client/update')
|
jest.mock('../../src/client/update')
|
||||||
jest.mock('../../package.json', () => ({
|
jest.mock('../../package.json', () => ({
|
||||||
version: 'test-version'
|
version: 'test-version',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe('plugin', () => {
|
describe('plugin', () => {
|
||||||
@@ -56,8 +56,8 @@ describe('plugin', () => {
|
|||||||
const Component = Vue.component('test-component', {
|
const Component = Vue.component('test-component', {
|
||||||
template: '<div>Test</div>',
|
template: '<div>Test</div>',
|
||||||
[defaultOptions.keyName]: {
|
[defaultOptions.keyName]: {
|
||||||
title: 'Hello World'
|
title: 'Hello World',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { vm } = mount(Component, { localVue: Vue })
|
const { vm } = mount(Component, { localVue: Vue })
|
||||||
@@ -86,8 +86,8 @@ describe('plugin', () => {
|
|||||||
const Component = Vue.component('test-component', {
|
const Component = Vue.component('test-component', {
|
||||||
template: '<div>Test</div>',
|
template: '<div>Test</div>',
|
||||||
[defaultOptions.keyName]: {
|
[defaultOptions.keyName]: {
|
||||||
title: 'Hello World'
|
title: 'Hello World',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
Vue.config.devtools = true
|
Vue.config.devtools = true
|
||||||
@@ -107,8 +107,8 @@ describe('plugin', () => {
|
|||||||
const Component = Vue.component('test-component', {
|
const Component = Vue.component('test-component', {
|
||||||
template: '<div>Test</div>',
|
template: '<div>Test</div>',
|
||||||
[defaultOptions.keyName]: {
|
[defaultOptions.keyName]: {
|
||||||
title: 'Hello World'
|
title: 'Hello World',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { vm } = mount(Component, { localVue: Vue })
|
const { vm } = mount(Component, { localVue: Vue })
|
||||||
@@ -122,13 +122,15 @@ describe('plugin', () => {
|
|||||||
test('can use generate export with options', () => {
|
test('can use generate export with options', () => {
|
||||||
process.server = true
|
process.server = true
|
||||||
const rawInfo = {
|
const rawInfo = {
|
||||||
meta: [{ charset: 'utf-8' }]
|
meta: [{ charset: 'utf-8' }],
|
||||||
}
|
}
|
||||||
|
|
||||||
const metaInfo = VueMetaPlugin.generate(rawInfo, {
|
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
|
// no error on not provided metaInfo types
|
||||||
expect(metaInfo.script.text()).toBe('')
|
expect(metaInfo.script.text()).toBe('')
|
||||||
@@ -139,50 +141,56 @@ describe('plugin', () => {
|
|||||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||||
|
|
||||||
const rawInfo = {
|
const rawInfo = {
|
||||||
meta: [{ charset: 'utf-8' }]
|
meta: [{ charset: 'utf-8' }],
|
||||||
}
|
}
|
||||||
|
|
||||||
const metaInfo = VueMetaPlugin.generate(rawInfo)
|
const metaInfo = VueMetaPlugin.generate(rawInfo)
|
||||||
expect(metaInfo).toBeUndefined()
|
expect(metaInfo).toBeUndefined()
|
||||||
expect(warn).toHaveBeenCalledTimes(1)
|
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()
|
warn.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('updates can be paused and resumed', async () => {
|
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)
|
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
|
||||||
// because triggerUpdate & batchUpdate reside in the same file we cant mock them both,
|
// because triggerUpdate & batchUpdate reside in the same file we cant mock them both,
|
||||||
// so just recreate the triggerUpdate fn by copying its implementation
|
// so just recreate the triggerUpdate fn by copying its implementation
|
||||||
const triggerUpdateSpy = triggerUpdate.mockImplementation((options, vm, hookName) => {
|
const triggerUpdateSpy = triggerUpdate.mockImplementation(
|
||||||
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.pausing) {
|
(options, vm, hookName) => {
|
||||||
// batch potential DOM updates to prevent extraneous re-rendering
|
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.pausing) {
|
||||||
batchUpdateSpy(() => vm.$meta().refresh())
|
// batch potential DOM updates to prevent extraneous re-rendering
|
||||||
|
batchUpdateSpy(() => vm.$meta().refresh())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
const Component = Vue.component('test-component', {
|
const Component = Vue.component('test-component', {
|
||||||
metaInfo () {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
title: this.title
|
title: this.title,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
template: '<div>Test</div>'
|
template: '<div>Test</div>',
|
||||||
})
|
})
|
||||||
|
|
||||||
let title = 'first title'
|
let title = 'first title'
|
||||||
const wrapper = mount(Component, {
|
const wrapper = mount(Component, {
|
||||||
localVue: Vue,
|
localVue: Vue,
|
||||||
propsData: {
|
propsData: {
|
||||||
title
|
title,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// no batchUpdate on initialization
|
// no batchUpdate on initialization
|
||||||
@@ -223,7 +231,9 @@ describe('plugin', () => {
|
|||||||
test('updates are batched by default', async () => {
|
test('updates are batched by default', async () => {
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
|
|
||||||
const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update')
|
const { batchUpdate: _batchUpdate } = jest.requireActual(
|
||||||
|
'../../src/client/update'
|
||||||
|
)
|
||||||
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
|
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
|
||||||
const refreshSpy = jest.fn()
|
const refreshSpy = jest.fn()
|
||||||
// because triggerUpdate & batchUpdate reside in the same file we cant mock them both,
|
// 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', {
|
const Component = Vue.component('test-component', {
|
||||||
metaInfo () {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
title: this.title
|
title: this.title,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: '',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
template: '<div>Test</div>'
|
template: '<div>Test</div>',
|
||||||
})
|
})
|
||||||
|
|
||||||
let title = 'first title'
|
let title = 'first title'
|
||||||
const wrapper = mount(Component, {
|
const wrapper = mount(Component, {
|
||||||
localVue: Vue,
|
localVue: Vue,
|
||||||
propsData: {
|
propsData: {
|
||||||
title
|
title,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
await vmTick(wrapper.vm)
|
await vmTick(wrapper.vm)
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ describe('shared', () => {
|
|||||||
const componentMock = {
|
const componentMock = {
|
||||||
_vueMeta: {
|
_vueMeta: {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
pausing: false
|
pausing: false,
|
||||||
},
|
},
|
||||||
$meta: () => ({ refresh })
|
$meta: () => ({ refresh }),
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerUpdate({ debounceWait: 0 }, componentMock, 'test')
|
triggerUpdate({ debounceWait: 0 }, componentMock, 'test')
|
||||||
|
|||||||
+31
-12
@@ -1,10 +1,15 @@
|
|||||||
import _updateClientMetaInfo from '../../src/client/updateClientMetaInfo'
|
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 metaInfoData from '../utils/meta-info-data'
|
||||||
import * as load from '../../src/client/load'
|
import * as load from '../../src/client/load'
|
||||||
import { clearClientAttributeMap } from '../utils'
|
import { clearClientAttributeMap } from '../utils'
|
||||||
|
|
||||||
const updateClientMetaInfo = (type, data) => _updateClientMetaInfo(ssrAppId, defaultOptions, { [type]: data })
|
const updateClientMetaInfo = (type, data) =>
|
||||||
|
_updateClientMetaInfo(ssrAppId, defaultOptions, { [type]: data })
|
||||||
|
|
||||||
describe('updaters', () => {
|
describe('updaters', () => {
|
||||||
let html
|
let html
|
||||||
@@ -13,22 +18,26 @@ describe('updaters', () => {
|
|||||||
html = document.getElementsByTagName('html')[0]
|
html = document.getElementsByTagName('html')[0]
|
||||||
|
|
||||||
// remove default meta charset
|
// 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) {
|
for (const type in metaInfoData) {
|
||||||
const typeTests = metaInfoData[type]
|
const typeTests = metaInfoData[type]
|
||||||
|
|
||||||
const testCases = {
|
const testCases = {
|
||||||
add: (tags) => {
|
add: tags => {
|
||||||
typeTests.add.expect.forEach((expected, index) => {
|
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(tags.tagsAdded[type][index].outerHTML).toBe(expected)
|
||||||
}
|
}
|
||||||
expect(html.outerHTML).toContain(expected)
|
expect(html.outerHTML).toContain(expected)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
change: (tags) => {
|
change: tags => {
|
||||||
typeTests.add.expect.forEach((expected, index) => {
|
typeTests.add.expect.forEach((expected, index) => {
|
||||||
if (!typeTests.change.expect.includes(expected)) {
|
if (!typeTests.change.expect.includes(expected)) {
|
||||||
expect(html.outerHTML).not.toContain(expected)
|
expect(html.outerHTML).not.toContain(expected)
|
||||||
@@ -36,13 +45,15 @@ describe('updaters', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
typeTests.change.expect.forEach((expected, index) => {
|
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(tags.tagsAdded[type][index].outerHTML).toBe(expected)
|
||||||
}
|
}
|
||||||
expect(html.outerHTML).toContain(expected)
|
expect(html.outerHTML).toContain(expected)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
remove: (tags) => {
|
remove: tags => {
|
||||||
// TODO: i'd expect tags.removedTags to be populated
|
// TODO: i'd expect tags.removedTags to be populated
|
||||||
typeTests.add.expect.forEach((expected, index) => {
|
typeTests.add.expect.forEach((expected, index) => {
|
||||||
expect(html.outerHTML).not.toContain(expected)
|
expect(html.outerHTML).not.toContain(expected)
|
||||||
@@ -53,13 +64,13 @@ describe('updaters', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(html.outerHTML).not.toContain(`<${type}`)
|
expect(html.outerHTML).not.toContain(`<${type}`)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
describe(`${type} type tests`, () => {
|
describe(`${type} type tests`, () => {
|
||||||
beforeAll(() => clearClientAttributeMap())
|
beforeAll(() => clearClientAttributeMap())
|
||||||
|
|
||||||
Object.keys(typeTests).forEach((action) => {
|
Object.keys(typeTests).forEach(action => {
|
||||||
const testInfo = typeTests[action]
|
const testInfo = typeTests[action]
|
||||||
|
|
||||||
// return when no test case available
|
// return when no test case available
|
||||||
@@ -102,12 +113,20 @@ describe('updaters', () => {
|
|||||||
describe('extra tests', () => {
|
describe('extra tests', () => {
|
||||||
test('adds callback listener on hydration', () => {
|
test('adds callback listener on hydration', () => {
|
||||||
const addListeners = load.addListeners
|
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]
|
const html = document.getElementsByTagName('html')[0]
|
||||||
html.setAttribute(ssrAttribute, 'true')
|
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)
|
const tags = updateClientMetaInfo('script', data)
|
||||||
|
|
||||||
expect(tags).toBe(false)
|
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