2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-12 22:32:27 +03:00

feat: convert to ts (wip)

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

Some files were not shown because too many files have changed in this diff Show More