2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-25 07:30:34 +03:00

test: add render tests

fix: webpack dev server

chore: use eslint not prettier

unfeat: remove support for comments (its brokenn in Vue, maybe later)
This commit is contained in:
pimlie
2020-07-26 00:11:28 +02:00
parent 28d3fc1923
commit 27aaf4744a
80 changed files with 2785 additions and 12109 deletions
+2
View File
@@ -0,0 +1,2 @@
test/old
__build__
+18
View File
@@ -0,0 +1,18 @@
{
"extends": [
"@nuxtjs/eslint-config-typescript"
],
"globals": {
"__DEV__": true
},
"overrides": [
{
"files": [
"examples/**"
],
"rules": {
"no-console": "off",
}
}
]
}
-5
View File
@@ -1,5 +0,0 @@
node_modules
__build__
dist
.vue-meta
_old
-6
View File
@@ -1,6 +0,0 @@
{
"semi": false,
"trailingComma": "es5",
"singleQuote": true,
"arrowParens": "avoid"
}
+14 -1
View File
@@ -1,4 +1,16 @@
module.exports = { module.exports = {
presets: [
["@babel/preset-env", {
useBuiltIns: 'usage',
corejs: 3,
targets: {
ie: 9
}
}]
],
plugins: [
"dynamic-import-node"
],
env: { env: {
test: { test: {
plugins: [ plugins: [
@@ -10,7 +22,8 @@ module.exports = {
targets: { targets: {
node: 'current' node: 'current'
} }
}] }],
'@babel/preset-typescript'
], ],
} }
} }
-1924
View File
File diff suppressed because it is too large Load Diff
-1550
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-1920
View File
File diff suppressed because it is too large Load Diff
-1643
View File
File diff suppressed because it is too large Load Diff
-10
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -10,14 +10,14 @@ window.users.push({
zipcode: '92998-3874', zipcode: '92998-3874',
geo: { geo: {
lat: '-37.3159', lat: '-37.3159',
lng: '81.1496', lng: '81.1496'
}, }
}, },
phone: '1-770-736-8031 x56442', phone: '1-770-736-8031 x56442',
website: 'hildegard.org', website: 'hildegard.org',
company: { company: {
name: 'Romaguera-Crona', name: 'Romaguera-Crona',
catchPhrase: 'Multi-layered client-server neural-net', catchPhrase: 'Multi-layered client-server neural-net',
bs: 'harness real-time e-markets', bs: 'harness real-time e-markets'
}, }
}) })
+4 -4
View File
@@ -10,14 +10,14 @@ window.users.push({
zipcode: '90566-7771', zipcode: '90566-7771',
geo: { geo: {
lat: '-43.9509', lat: '-43.9509',
lng: '-34.4618', lng: '-34.4618'
}, }
}, },
phone: '010-692-6593 x09125', phone: '010-692-6593 x09125',
website: 'anastasia.net', website: 'anastasia.net',
company: { company: {
name: 'Deckow-Crist', name: 'Deckow-Crist',
catchPhrase: 'Proactive didactic contingency', catchPhrase: 'Proactive didactic contingency',
bs: 'synergize scalable supply-chains', bs: 'synergize scalable supply-chains'
}, }
}) })
+4 -4
View File
@@ -10,14 +10,14 @@ window.users.push({
zipcode: '59590-4157', zipcode: '59590-4157',
geo: { geo: {
lat: '-68.6102', lat: '-68.6102',
lng: '-47.0653', lng: '-47.0653'
}, }
}, },
phone: '1-463-123-4447', phone: '1-463-123-4447',
website: 'ramiro.info', website: 'ramiro.info',
company: { company: {
name: 'Romaguera-Jacobson', name: 'Romaguera-Jacobson',
catchPhrase: 'Face to face bifurcated interface', catchPhrase: 'Face to face bifurcated interface',
bs: 'e-enable strategic applications', bs: 'e-enable strategic applications'
}, }
}) })
+4 -4
View File
@@ -10,14 +10,14 @@ window.users.push({
zipcode: '53919-4257', zipcode: '53919-4257',
geo: { geo: {
lat: '29.4572', lat: '29.4572',
lng: '-164.2990', lng: '-164.2990'
}, }
}, },
phone: '493-170-9623 x156', phone: '493-170-9623 x156',
website: 'kale.biz', website: 'kale.biz',
company: { company: {
name: 'Robel-Corkery', name: 'Robel-Corkery',
catchPhrase: 'Multi-tiered zero tolerance productivity', catchPhrase: 'Multi-tiered zero tolerance productivity',
bs: 'transition cutting-edge web services', bs: 'transition cutting-edge web services'
}, }
}) })
+16 -16
View File
@@ -6,7 +6,7 @@ Vue.use(VueMeta)
window.users = [] window.users = []
new Vue({ new Vue({
metaInfo() { metaInfo () {
return { return {
title: 'Async Callback', title: 'Async Callback',
titleTemplate: '%s | Vue Meta Examples', titleTemplate: '%s | Vue Meta Examples',
@@ -16,56 +16,56 @@ new Vue({
vmid: 'potatoes', vmid: 'potatoes',
src: '/user-3.js', src: '/user-3.js',
async: true, async: true,
callback: this.updateCounter, callback: this.updateCounter
}, },
{ {
skip: this.count < 1, skip: this.count < 1,
vmid: 'vegetables', vmid: 'vegetables',
src: '/user-2.js', src: '/user-2.js',
async: true, async: true,
callback: this.updateCounter, callback: this.updateCounter
}, },
{ {
vmid: 'meat', vmid: 'meat',
src: '/user-1.js', src: '/user-1.js',
async: true, async: true,
callback: el => this.loadCallback(el.getAttribute('data-vmid')), callback: el => this.loadCallback(el.getAttribute('data-vmid'))
}, },
...this.scripts, ...this.scripts
], ]
} }
}, },
data() { data () {
return { return {
count: 0, count: 0,
scripts: [], scripts: [],
users: window.users, users: window.users
} }
}, },
watch: { watch: {
count(val) { count (val) {
if (val === 3) { if (val === 3) {
this.addScript() this.addScript()
} }
}, }
}, },
methods: { methods: {
updateCounter() { updateCounter () {
this.count++ this.count++
}, },
addScript() { addScript () {
this.scripts.push({ this.scripts.push({
src: '/user-4.js', src: '/user-4.js',
callback: () => { callback: () => {
this.updateCounter() this.updateCounter()
}, }
}) })
}, },
loadCallback(vmid) { loadCallback (vmid) {
if (vmid === 'meat') { if (vmid === 'meat') {
this.updateCounter() this.updateCounter()
} }
}, }
}, },
template: ` template: `
<div id="app"> <div id="app">
@@ -84,5 +84,5 @@ new Vue({
</ul> </ul>
</div> </div>
</div> </div>
`, `
}).$mount('#app') }).$mount('#app')
+10 -10
View File
@@ -8,17 +8,17 @@ Vue.component('child', {
props: { props: {
page: { page: {
type: String, type: String,
default: '', default: ''
},
},
render(h) {
return h('h3', null, this.page)
},
metaInfo() {
return {
title: this.page,
} }
}, },
render (h) {
return h('h3', null, this.page)
},
metaInfo () {
return {
title: this.page
}
}
}) })
new Vue({ new Vue({
@@ -28,5 +28,5 @@ new Vue({
<p>Inspect Element to see the meta info</p> <p>Inspect Element to see the meta info</p>
<child page="This is a prop"></child> <child page="This is a prop"></child>
</div> </div>
`, `
}).$mount('#app') }).$mount('#app')
+7 -7
View File
@@ -15,24 +15,24 @@ new Vue({
titleTemplate: '%s | Vue Meta Examples', titleTemplate: '%s | Vue Meta Examples',
htmlAttrs: { htmlAttrs: {
lang: 'en', lang: 'en',
amp: undefined, amp: undefined
}, },
headAttrs: { headAttrs: {
test: true, test: true
}, },
meta: [{ name: 'description', content: 'Hello', vmid: 'test' }], meta: [{ name: 'description', content: 'Hello', vmid: 'test' }],
script: [ script: [
{ {
innerHTML: innerHTML:
'{ "@context": "http://www.schema.org", "@type": "Organization" }', '{ "@context": "http://www.schema.org", "@type": "Organization" }',
type: 'application/ld+json', type: 'application/ld+json'
}, },
{ {
innerHTML: '{ "body": "yes" }', innerHTML: '{ "body": "yes" }',
body: true, body: true,
type: 'application/ld+json', type: 'application/ld+json'
}, }
], ],
__dangerouslyDisableSanitizers: ['script'], __dangerouslyDisableSanitizers: ['script']
}), })
}).$mount('#app') }).$mount('#app')
+7 -7
View File
@@ -6,18 +6,18 @@ Vue.use(VueMeta)
Vue.component('foo', { Vue.component('foo', {
template: '<p>Foo component</p>', template: '<p>Foo component</p>',
metaInfo: { metaInfo: {
title: 'Keep me Foo', title: 'Keep me Foo'
}, }
}) })
new Vue({ new Vue({
data() { data () {
return { showFoo: false } return { showFoo: false }
}, },
methods: { methods: {
show() { show () {
this.showFoo = !this.showFoo this.showFoo = !this.showFoo
}, }
}, },
template: ` template: `
<div id="app"> <div id="app">
@@ -29,6 +29,6 @@ new Vue({
</div> </div>
`, `,
metaInfo: () => ({ metaInfo: () => ({
title: 'Keep-alive', title: 'Keep-alive'
}), })
}).$mount('#app') }).$mount('#app')
+4 -4
View File
@@ -5,12 +5,12 @@ const {
processIf, processIf,
getBaseTransformPreset, getBaseTransformPreset,
createObjectExpression, createObjectExpression,
createObjectProperty, createObjectProperty
} = require('@vue/compiler-core') } = require('@vue/compiler-core')
const { parse } = require('@vue/compiler-dom') const { parse } = require('@vue/compiler-dom')
function headTransform(node, context) { function headTransform (node, context) {
console.log('NODE', node) console.log('NODE', node)
if (node.type === 1 /* NodeTypes.ELEMENT */) { if (node.type === 1 /* NodeTypes.ELEMENT */) {
return () => { return () => {
@@ -35,13 +35,13 @@ module.exports = function (source, map) {
// console.log('AST', ast) // console.log('AST', ast)
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset({ const [nodeTransforms, directiveTransforms] = getBaseTransformPreset({
prefixIdentifiers: true, prefixIdentifiers: true
}) })
transform(ast, { transform(ast, {
prefixIdentifiers: true, prefixIdentifiers: true,
nodeTransforms: [...nodeTransforms, headTransform], nodeTransforms: [...nodeTransforms, headTransform],
directiveTransforms, directiveTransforms
}) })
const result = generate(ast, { mode: 'module' }) const result = generate(ast, { mode: 'module' })
+14 -14
View File
@@ -6,50 +6,50 @@ Vue.use(VueMeta)
// index.html contains a manual SSR render // index.html contains a manual SSR render
const app1 = new Vue({ const app1 = new Vue({
metaInfo() { metaInfo () {
return { return {
title: 'App 1 title', title: 'App 1 title',
bodyAttrs: { bodyAttrs: {
class: 'app-1', class: 'app-1'
}, },
meta: [ meta: [
{ name: 'description', content: 'Hello from app 1', vmid: 'test' }, { name: 'description', content: 'Hello from app 1', vmid: 'test' },
{ name: 'og:description', content: this.ogContent }, { name: 'og:description', content: this.ogContent }
], ],
script: [ script: [
{ innerHTML: 'var appId=1.1', body: true }, { innerHTML: 'var appId=1.1', body: true },
{ innerHTML: 'var appId=1.2', vmid: 'app-id-body' }, { innerHTML: 'var appId=1.2', vmid: 'app-id-body' }
], ]
} }
}, },
data() { data () {
return { return {
ogContent: 'Hello from ssr app', ogContent: 'Hello from ssr app'
} }
}, },
template: ` template: `
<div id="app1"><h1>App 1</h1></div> <div id="app1"><h1>App 1</h1></div>
`, `
}) })
const app2 = new Vue({ const app2 = new Vue({
metaInfo: () => ({ metaInfo: () => ({
title: 'App 2 title', title: 'App 2 title',
bodyAttrs: { bodyAttrs: {
class: 'app-2', class: 'app-2'
}, },
meta: [ meta: [
{ name: 'description', content: 'Hello from app 2', vmid: 'test' }, { name: 'description', content: 'Hello from app 2', vmid: 'test' },
{ name: 'og:description', content: 'Hello from app 2' }, { name: 'og:description', content: 'Hello from app 2' }
], ],
script: [ script: [
{ innerHTML: 'var appId=2.1', body: true }, { innerHTML: 'var appId=2.1', body: true },
{ innerHTML: 'var appId=2.2', vmid: 'app-id-body', body: true }, { innerHTML: 'var appId=2.2', vmid: 'app-id-body', body: true }
], ]
}), }),
template: ` template: `
<div id="app2"><h1>App 2</h1></div> <div id="app2"><h1>App 2</h1></div>
`, `
}).$mount('#app2') }).$mount('#app2')
app1.$mount('#app1') app1.$mount('#app1')
@@ -57,7 +57,7 @@ app1.$mount('#app1')
const app3 = new Vue({ const app3 = new Vue({
template: ` template: `
<div id="app3"><h1>App 3 (empty metaInfo)</h1></div> <div id="app3"><h1>App 3 (empty metaInfo)</h1></div>
`, `
}).$mount('#app3') }).$mount('#app3')
setTimeout(() => { setTimeout(() => {
+3 -3
View File
@@ -16,14 +16,14 @@ app.use(
writeToDisk: true, writeToDisk: true,
stats: { stats: {
colors: true, colors: true,
chunks: false, chunks: false
}, }
}) })
) )
fs.readdirSync(__dirname) fs.readdirSync(__dirname)
.filter(file => file !== 'ssr') .filter(file => file !== 'ssr')
.forEach(file => { .forEach((file) => {
if (fs.statSync(path.join(__dirname, file)).isDirectory()) { if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
app.use(rewrite(`/${file}/*`, `/${file}/index.html`)) app.use(rewrite(`/${file}/*`, `/${file}/index.html`))
} }
+31 -31
View File
@@ -6,7 +6,7 @@ Vue.use(VueMeta, {
tagIDKeyName: 'hid' tagIDKeyName: 'hid'
}) */ }) */
export default function createMyApp() { export default function createMyApp () {
const Home = { const Home = {
template: `<div> template: `<div>
<router-link to="/about">About</router-link> <router-link to="/about">About</router-link>
@@ -19,15 +19,15 @@ export default function createMyApp() {
{ {
hid: 'og:title', hid: 'og:title',
name: 'og:title', name: 'og:title',
content: 'Hello World', content: 'Hello World'
}, },
{ {
hid: 'description', hid: 'description',
name: 'description', name: 'description',
content: 'Hello World', content: 'Hello World'
}, }
], ]
}, }
} }
const About = { const About = {
@@ -42,28 +42,28 @@ export default function createMyApp() {
{ {
hid: 'og:title', hid: 'og:title',
name: 'og:title', name: 'og:title',
content: 'About World', content: 'About World'
}, },
{ {
hid: 'description', hid: 'description',
name: 'description', name: 'description',
content: 'About World', content: 'About World'
}, }
], ]
}, }
} }
const router = createRouter({ const router = createRouter({
history: createMemoryHistory('/ssr'), history: createMemoryHistory('/ssr'),
routes: [ routes: [
{ path: '/', component: Home }, { path: '/', component: Home },
{ path: '/about', component: About }, { path: '/about', component: About }
], ]
}) })
const app = createSSRApp({ const app = createSSRApp({
router, router,
metaInfo() { metaInfo () {
return { return {
title: 'Boring Title', title: 'Boring Title',
htmlAttrs: { amp: true }, htmlAttrs: { amp: true },
@@ -74,63 +74,63 @@ export default function createMyApp() {
hid: 'og:title', hid: 'og:title',
name: 'og:title', name: 'og:title',
template: chunk => `${chunk} - My Site`, template: chunk => `${chunk} - My Site`,
content: 'Default Title', content: 'Default Title'
}, },
{ {
hid: 'description', hid: 'description',
name: 'description', name: 'description',
content: 'Say something', content: 'Say something'
}, }
], ],
script: [ script: [
{ {
hid: 'ldjson-schema', hid: 'ldjson-schema',
type: 'application/ld+json', type: 'application/ld+json',
innerHTML: innerHTML:
'{ "@context": "http://www.schema.org", "@type": "Organization" }', '{ "@context": "http://www.schema.org", "@type": "Organization" }'
}, },
{ {
type: 'application/ld+json', type: 'application/ld+json',
innerHTML: '{ "body": "yes" }', innerHTML: '{ "body": "yes" }',
body: true, body: true
}, },
{ {
hid: 'my-async-script-with-load-callback', hid: 'my-async-script-with-load-callback',
src: '/user-1.js', src: '/user-1.js',
body: true, body: true,
defer: true, defer: true,
callback: this.loadCallback, callback: this.loadCallback
}, },
{ {
skip: this.count < 1, skip: this.count < 1,
src: '/user-2.js', src: '/user-2.js',
body: true, body: true,
callback: this.loadCallback, callback: this.loadCallback
}, }
], ],
__dangerouslyDisableSanitizersByTagID: { __dangerouslyDisableSanitizersByTagID: {
'ldjson-schema': ['innerHTML'], 'ldjson-schema': ['innerHTML']
}, }
} }
}, },
data() { data () {
return { return {
count: 0, count: 0,
users: process.server ? [] : window.users, users: process.server ? [] : window.users
} }
}, },
mounted() { mounted () {
const { set, remove } = this.$meta().addApp('client-only') const { set, remove } = this.$meta().addApp('client-only')
set({ set({
bodyAttrs: { class: 'client-only' }, bodyAttrs: { class: 'client-only' }
}) })
setTimeout(() => remove(), 3000) setTimeout(() => remove(), 3000)
}, },
methods: { methods: {
loadCallback() { loadCallback () {
this.count++ this.count++
}, }
}, },
template: ` template: `
<div id="app"> <div id="app">
@@ -146,7 +146,7 @@ export default function createMyApp() {
</ul> </ul>
<router-view /> <router-view />
</div>`, </div>`
}) })
app.use(router) app.use(router)
+5 -5
View File
@@ -12,7 +12,7 @@ const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
process.server = true process.server = true
export async function renderPage({ url }) { export async function renderPage ({ url }) {
const { app, router } = await createApp() const { app, router } = await createApp()
await router.push(url.substr(4)) await router.push(url.substr(4))
@@ -30,17 +30,17 @@ export async function renderPage({ url }) {
const pageHtml = compiled({ const pageHtml = compiled({
app: appHtml, app: appHtml,
htmlAttrs: { htmlAttrs: {
text: () => {}, text: () => {}
}, },
headAttrs: { headAttrs: {
text: () => {}, text: () => {}
}, },
bodyAttrs: { bodyAttrs: {
text: () => {}, text: () => {}
}, },
head: () => {}, head: () => {},
bodyPrepend: () => {}, bodyPrepend: () => {},
bodyAppend: () => {}, bodyAppend: () => {}
// ...app.$meta().inject() // ...app.$meta().inject()
}) })
-2
View File
@@ -27,5 +27,3 @@ export default {
} }
} }
</script> </script>
useMeta()
+51 -51
View File
@@ -6,10 +6,10 @@ import {
inject, inject,
toRefs, toRefs,
h, h,
watch, watch
} from 'vue' } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { createManager, useMeta, useMetainfo } from '../../src' import { createManager, useMeta, useMetainfo } from 'vue-meta'
// import About from './about.vue' // import About from './about.vue'
const metaUpdated = 'no' const metaUpdated = 'no'
@@ -17,47 +17,47 @@ const metaUpdated = 'no'
const ChildComponent = defineComponent({ const ChildComponent = defineComponent({
name: 'child-component', name: 'child-component',
props: { props: {
page: String, page: String
}, },
template: ` template: `
<div> <div>
<h3>You're looking at the <strong>{{ page }}</strong> page</h3> <h3>You're looking at the <strong>{{ page }}</strong> page</h3>
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p> <p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
</div>`, </div>`,
setup(props) { setup (props) {
const state = reactive({ const state = reactive({
date: null, date: null,
metaUpdated, metaUpdated
}) })
const title = props.page[0].toUpperCase() + props.page.slice(1) const title = props.page[0].toUpperCase() + props.page.slice(1)
console.log('ChildComponent Setup') console.log('ChildComponent Setup')
useMeta({ /* useMeta({
charset: 'utf16', charset: 'utf16',
title, title,
description: 'Description ' + props.page, description: 'Description ' + props.page,
og: { og: {
title: 'Og Title ' + props.page, title: 'Og Title ' + props.page,
}, },
}) }) */
return { return {
...toRefs(state), ...toRefs(state)
} }
}, }
}) })
function view(page) { function view (page) {
return { return {
name: `section-${page}`, name: `section-${page}`,
render() { render () {
return h(ChildComponent, { page }) return h(ChildComponent, { page })
}, }
} }
} }
const App = { const App = {
setup() { setup () {
// console.log('App', getCurrentInstance()) // console.log('App', getCurrentInstance())
const { meta } = useMeta({ const { meta } = useMeta({
base: { href: '/vue-router', target: '_blank' }, base: { href: '/vue-router', target: '_blank' },
@@ -69,69 +69,69 @@ const App = {
description: 'Bla bla', description: 'Bla bla',
image: [ image: [
'https://picsum.photos/600/400/?image=80', 'https://picsum.photos/600/400/?image=80',
'https://picsum.photos/600/400/?image=82', 'https://picsum.photos/600/400/?image=82'
], ]
}, },
twitter: { twitter: {
title: 'Twitter Title', title: 'Twitter Title'
}, },
noscript: [ noscript: [
'<!-- // A code comment -->', //'<!-- // A code comment -->',
{ tag: 'link', rel: 'stylesheet', href: 'style.css' }, { tag: 'link', rel: 'stylesheet', href: 'style.css' }
], ],
otherNoscript: { otherNoscript: {
tag: 'noscript', tag: 'noscript',
'data-test': 'hello', 'data-test': 'hello',
content: [ children: [
'<!-- // Another code comment -->', //'<!-- // Another code comment -->',
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' }, { tag: 'link', rel: 'stylesheet', href: 'style2.css' }
], ]
}, },
body: 'body-script1.js', body: 'body-script1.js',
script: [ script: [
'<!--[if IE]>', //'<!--[if IE]>',
{ src: 'head-script1.js' }, { src: 'head-script1.js' },
'<![endif]-->', //'<![endif]-->',
{ src: 'body-script2.js', target: 'body' }, { src: 'body-script2.js', to: 'body' },
{ src: 'body-script3.js', target: '#put-it-here' }, { src: 'body-script3.js', to: '#put-it-here' }
], ],
esi: { esi: {
content: [ children: [
{ {
tag: 'choose', tag: 'choose',
content: [ children: [
{ {
tag: 'when', tag: 'when',
test: '$(HTTP_COOKIE{group})=="Advanced"', test: '$(HTTP_COOKIE{group})=="Advanced"',
content: [ children: [
{ {
tag: 'include', tag: 'include',
src: 'http://www.example.com/advanced.html', src: 'http://www.example.com/advanced.html'
}, }
], ]
}, }
], ]
}, }
], ]
}, }
}) })
setTimeout(() => (meta.title = 'My Updated Title'), 2000) setTimeout(() => (meta.title = 'My Updated Title'), 2000)
const metainfo = useMetainfo() const metadata = useMetainfo()
window.$metainfo = metainfo window.$metainfo = metadata
watch(metainfo, (newValue, oldValue) => { watch(metadata, (newValue, oldValue) => {
console.log('UPDATE', newValue) console.log('UPDATE', newValue)
}) })
return { return {
metainfo, metadata
} }
}, },
template: ` template: `
<metainfo :metainfo="metainfo"> <metainfo :metainfo="metadata">
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template> <template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template>
<template v-slot:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template> <template v-slot:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
<template v-slot:og(title)="{ content, metainfo, og }"> <template v-slot:og(title)="{ content, metainfo, og }">
@@ -148,10 +148,10 @@ const App = {
</transition> </transition>
<p>Inspect Element to see the meta info</p> <p>Inspect Element to see the meta info</p>
</div> </div>
`, `
} }
function decisionMaker5000000(key, pathSegments, getOptions, getCurrentValue) { function decisionMaker5000000 (key, pathSegments, getOptions, getCurrentValue) {
let theChosenOne let theChosenOne
const options = getOptions() const options = getOptions()
@@ -177,26 +177,26 @@ const metaManager = createManager({
esi: { esi: {
group: true, group: true,
namespaced: true, namespaced: true,
contentAttributes: ['src', 'test', 'text'], attributes: ['src', 'test', 'text']
}, }
}, }
}) })
useMeta( /* useMeta(
{ {
og: { og: {
something: 'test', something: 'test',
}, },
}, },
metaManager metaManager
) ) */
const router = createRouter({ const router = createRouter({
history: createWebHistory('/vue-router'), history: createWebHistory('/vue-router'),
routes: [ routes: [
{ name: 'home', path: '/', component: view('home') }, { name: 'home', path: '/', component: view('home') },
{ name: 'about', path: '/about', component: view('about') }, { name: 'about', path: '/about', component: view('about') }
], ]
}) })
const app = createApp(App) const app = createApp(App)
+2 -2
View File
@@ -12,6 +12,6 @@ export default new Router({
base: '/vuex-async', base: '/vuex-async',
routes: [ routes: [
{ path: '/', component: Home }, { path: '/', component: Home },
{ path: '/posts/:slug', component: Post }, { path: '/posts/:slug', component: Post }
], ]
}) })
+17 -17
View File
@@ -14,64 +14,64 @@ export default new Vuex.Store({
title: '', title: '',
content: '', content: '',
slug: '', slug: '',
published: false, published: false
}, },
posts: [ posts: [
{ {
slug: 'a-sample-blog-post', slug: 'a-sample-blog-post',
title: 'A Sample Blog Post', title: 'A Sample Blog Post',
content: 'This is the blog post content', content: 'This is the blog post content',
published: true, published: true
}, },
{ {
slug: 'an-unpublished-blog-post', slug: 'an-unpublished-blog-post',
title: 'An Unpublished Blog Post', title: 'An Unpublished Blog Post',
content: 'This is the blog post content', content: 'This is the blog post content',
published: false, published: false
}, },
{ {
slug: 'another-blog-post', slug: 'another-blog-post',
title: 'Another Blog Post', title: 'Another Blog Post',
content: 'This is the blog post content', content: 'This is the blog post content',
published: true, published: true
}, }
], ]
}, },
// GETTERS // GETTERS
getters: { getters: {
isLoading(state) { isLoading (state) {
return state.isLoading return state.isLoading
}, },
post(state) { post (state) {
return state.post return state.post
}, },
publishedPosts(state) { publishedPosts (state) {
return state.posts.filter(post => post.published) return state.posts.filter(post => post.published)
}, },
publishedPostsCount(state, getters) { publishedPostsCount (state, getters) {
return getters.publishedPosts.length return getters.publishedPosts.length
}, }
}, },
// MUTATIONS // MUTATIONS
mutations: { mutations: {
loadingState(state, { isLoading }) { loadingState (state, { isLoading }) {
state.isLoading = isLoading state.isLoading = isLoading
}, },
getPost(state, { slug }) { getPost (state, { slug }) {
state.post = state.posts.find(post => post.slug === slug) state.post = state.posts.find(post => post.slug === slug)
}, }
}, },
// ACTIONS // ACTIONS
actions: { actions: {
getPost({ commit }, payload) { getPost ({ commit }, payload) {
commit('loadingState', { isLoading: true }) commit('loadingState', { isLoading: true })
setTimeout(() => { setTimeout(() => {
commit('getPost', payload) commit('getPost', payload)
commit('loadingState', { isLoading: false }) commit('loadingState', { isLoading: false })
}, 2000) }, 2000)
}, }
}, }
}) })
+2 -2
View File
@@ -13,6 +13,6 @@ export default new Router({
base: '/vuex', base: '/vuex',
routes: [ routes: [
{ path: '/', component: Home }, { path: '/', component: Home },
{ path: '/posts/:slug', component: Post }, { path: '/posts/:slug', component: Post }
], ]
}) })
+15 -15
View File
@@ -14,54 +14,54 @@ export default new Vuex.Store({
title: '', title: '',
content: '', content: '',
slug: '', slug: '',
published: false, published: false
}, },
posts: [ posts: [
{ {
slug: 'a-sample-blog-post', slug: 'a-sample-blog-post',
title: 'A Sample Blog Post', title: 'A Sample Blog Post',
content: 'This is the blog post content', content: 'This is the blog post content',
published: true, published: true
}, },
{ {
slug: 'an-unpublished-blog-post', slug: 'an-unpublished-blog-post',
title: 'An Unpublished Blog Post', title: 'An Unpublished Blog Post',
content: 'This is the blog post content', content: 'This is the blog post content',
published: false, published: false
}, },
{ {
slug: 'another-blog-post', slug: 'another-blog-post',
title: 'Another Blog Post', title: 'Another Blog Post',
content: 'This is the blog post content', content: 'This is the blog post content',
published: true, published: true
}, }
], ]
}, },
// GETTERS // GETTERS
getters: { getters: {
post(state) { post (state) {
return state.post return state.post
}, },
publishedPosts(state) { publishedPosts (state) {
return state.posts.filter(post => post.published) return state.posts.filter(post => post.published)
}, },
publishedPostsCount(state, getters) { publishedPostsCount (state, getters) {
return getters.publishedPosts.length return getters.publishedPosts.length
}, }
}, },
// MUTATIONS // MUTATIONS
mutations: { mutations: {
getPost(state, { slug }) { getPost (state, { slug }) {
state.post = state.posts.find(post => post.slug === slug) state.post = state.posts.find(post => post.slug === slug)
}, }
}, },
// ACTIONS // ACTIONS
actions: { actions: {
getPost({ commit }, payload) { getPost ({ commit }, payload) {
commit('getPost', payload) commit('getPost', payload)
}, }
}, }
}) })
+96
View File
@@ -0,0 +1,96 @@
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: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
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: {
extensions: ['.tsx', '.ts', '.js'],
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, '../src/')
}
},
// 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'),
__DEV__: process.env.NODE_ENV !== 'production'
})
],
devServer: {
inline: true,
hot: true,
stats: 'minimal',
contentBase: __dirname,
overlay: true
}
}
+7 -3
View File
@@ -14,7 +14,7 @@ module.exports = {
coverageDirectory: './coverage', coverageDirectory: './coverage',
collectCoverageFrom: [ collectCoverageFrom: [
'**/src/**/*.js' '**/src/**/*.[tj]s'
], ],
coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: [
@@ -30,7 +30,7 @@ module.exports = {
], ],
transform: { transform: {
'^.+\\.js$': 'babel-jest', '^.+\\.[tj]s$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest' '.*\\.(vue)$': 'vue-jest'
}, },
@@ -38,5 +38,9 @@ module.exports = {
'ts', 'ts',
'js', 'js',
'json' 'json'
] ],
globals: {
__DEV__: true
}
} }
+37 -23
View File
@@ -32,10 +32,10 @@
"scripts": { "scripts": {
"build": "rimraf dist && rollup -c scripts/rollup.config.js", "build": "rimraf dist && rollup -c scripts/rollup.config.js",
"coverage": "codecov", "coverage": "codecov",
"dev": "webpack-dev-server --mode=development", "dev": "babel-node examples/server.js",
"docs": "vuepress dev --host 0.0.0.0 --port 3000 docs", "docs": "vuepress dev --host 0.0.0.0 --port 3000 docs",
"docs:build": "vuepress build docs", "docs:build": "vuepress build docs",
"lint": "prettier -c --parser typescript \"{examples,src,test,e2e}/**/*.[jt]s?(x)\"", "lint": "eslint --ext .js,.ts examples src test",
"prerelease": "git checkout master && git pull -r", "prerelease": "git checkout master && git pull -r",
"release": "yarn lint && yarn test && standard-version", "release": "yarn lint && yarn test && standard-version",
"test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser", "test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser",
@@ -48,27 +48,40 @@
"vue": "next" "vue": "next"
}, },
"devDependencies": { "devDependencies": {
"@types/webpack": "^4.41.16", "@babel/core": "^7.10.5",
"@babel/node": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@nuxtjs/eslint-config-typescript": "^2.0.0",
"@types/webpack": "^4.41.21",
"@types/webpack-env": "^1.15.2", "@types/webpack-env": "^1.15.2",
"@vue/compiler-sfc": "^3.0.0-beta.14", "@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"@vue/compiler-sfc": "^3.0.0-rc.4",
"@vue/server-renderer": "^3.0.0-rc.4",
"@vue/server-test-utils": "^1.0.3", "@vue/server-test-utils": "^1.0.3",
"@vue/test-utils": "^1.0.3", "@vue/test-utils": "^1.0.3",
"@wishy-gift/html-include-chunks-webpack-plugin": "^0.1.5", "@wishy-gift/html-include-chunks-webpack-plugin": "^0.1.5",
"babel-jest": "^26.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
"browserstack-local": "^1.4.5", "browserstack-local": "^1.4.5",
"chromedriver": "^83.0.0", "chromedriver": "^84.0.1",
"codecov": "^3.7.0", "codecov": "^3.7.2",
"consola": "^2.14.0",
"eslint": "^7.5.0",
"express-urlrewrite": "^1.3.0",
"geckodriver": "^1.19.1", "geckodriver": "^1.19.1",
"html-webpack-plugin": "^4.3.0", "html-webpack-plugin": "^4.3.0",
"jest": "^26.0.1", "jest": "^26.1.0",
"jest-environment-jsdom": "^26.0.1", "jest-environment-jsdom": "^26.1.0",
"jest-environment-jsdom-global": "^2.0.2", "jest-environment-jsdom-global": "^2.0.4",
"jsdom": "^16.2.2", "jsdom": "^16.3.0",
"lodash": "^4.17.15", "lodash": "^4.17.19",
"node-env-file": "^0.1.8", "node-env-file": "^0.1.8",
"prettier": "^2.0.5", "puppeteer-core": "^5.2.1",
"puppeteer-core": "^3.2.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^2.11.2", "rollup": "^2.23.0",
"rollup-plugin-babel": "^4.4.0", "rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-json": "^4.0.0", "rollup-plugin-json": "^4.0.0",
@@ -76,20 +89,21 @@
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^6.1.0", "rollup-plugin-terser": "^6.1.0",
"rollup-plugin-typescript2": "^0.27.1", "rollup-plugin-typescript2": "^0.27.1",
"selenium-webdriver": "^4.0.0-alpha.5", "selenium-webdriver": "^4.0.0-alpha.7",
"standard-version": "^8.0.0", "standard-version": "^8.0.2",
"tib": "^0.7.4", "tib": "^0.7.4",
"ts-jest": "^26.1.0", "ts-jest": "^26.1.3",
"ts-loader": "^7.0.5", "ts-loader": "^8.0.1",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"typescript": "^3.9.3", "typescript": "^3.9.7",
"vue": "next", "vue": "next",
"vue-jest": "^3.0.5", "vue-jest": "^3.0.6",
"vue-loader": "^16.0.0-beta.2", "vue-loader": "^16.0.0-beta.2",
"vue-router": "next", "vue-router": "next",
"webpack": "^4.43.0", "webpack": "^4.44.0",
"webpack-bundle-analyzer": "^3.8.0", "webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0" "webpack-dev-server": "^3.11.0",
"webpackbar": "^4.0.0"
} }
} }
+21 -18
View File
@@ -8,12 +8,12 @@ export interface MetainfoProps {
metainfo: MetainfoActive metainfo: MetainfoActive
} }
export function addVnode(targets: any, target: string, vnode: VNode) { export function addVnode (teleports: any, to: string, vnode: VNode) {
if (!targets[target]) { if (!teleports[to]) {
targets[target] = [] teleports[to] = []
} }
targets[target].push(vnode) teleports[to].push(vnode)
} }
export const MetainfoImpl = defineComponent({ export const MetainfoImpl = defineComponent({
@@ -21,12 +21,12 @@ export const MetainfoImpl = defineComponent({
props: { props: {
metainfo: { metainfo: {
type: Object as PropType<MetainfoActive>, type: Object as PropType<MetainfoActive>,
required: true, required: true
}, }
}, },
setup({ metainfo }, { slots }) { setup ({ metainfo }, { slots }) {
return () => { return () => {
const targets: any = {} const teleports: any = {}
const manager = getCurrentManager() const manager = getCurrentManager()
@@ -39,26 +39,29 @@ export const MetainfoImpl = defineComponent({
metainfo[key], metainfo[key],
config config
) )
let defaultTarget = console.log('RENDERED VNODES', vnodes)
(key !== 'base' && metainfo[key].target) || config.target || 'head' const defaultTo =
(key !== 'base' && metainfo[key].to) || config.to || 'head'
if (isArray(vnodes)) { if (isArray(vnodes)) {
for (const { target, vnode } of vnodes) { for (const { to, vnode } of vnodes) {
addVnode(targets, target || defaultTarget, vnode) console.log('VNODE 1', vnode)
addVnode(teleports, to || defaultTo, vnode)
} }
continue continue
} }
const { target, vnode } = vnodes const { to, vnode } = vnodes
addVnode(targets, target || defaultTarget, vnode) console.log('VNODE 2', vnode)
addVnode(teleports, to || defaultTo, vnode)
} }
// console.log('TARGETS', targets) console.log('TARGETS', teleports)
return Object.keys(targets).map(target => { return Object.keys(teleports).map((to) => {
return h(Teleport, { to: target }, targets[target]) return h(Teleport, { to }, teleports[to])
}) })
} }
}, }
}) })
export const Metainfo = (MetainfoImpl as any) as { export const Metainfo = (MetainfoImpl as any) as {
+20 -25
View File
@@ -4,10 +4,10 @@ import { TODO } from './types'
export interface ConfigOption { export interface ConfigOption {
tag?: string tag?: string
target?: string to?: string
group?: boolean group?: boolean
nameAttribute?: string keyAttribute?: string
contentAttribute?: string valueAttribute?: string
nameless?: boolean nameless?: boolean
namespaced?: boolean namespaced?: boolean
namespacedAttribute?: boolean namespacedAttribute?: boolean
@@ -16,64 +16,59 @@ export interface ConfigOption {
const defaultMapping: { [key: string]: ConfigOption } = { const defaultMapping: { [key: string]: ConfigOption } = {
body: { body: {
tag: 'script', tag: 'script',
target: 'body', to: 'body'
}, },
base: { base: {
contentAttribute: 'href', valueAttribute: 'href'
}, },
charset: { charset: {
tag: 'meta', tag: 'meta',
nameless: true, nameless: true,
contentAttribute: 'charset', valueAttribute: 'charset'
}, },
description: { description: {
tag: 'meta', tag: 'meta'
}, },
og: { og: {
group: true, group: true,
namespacedAttribute: true, namespacedAttribute: true,
tag: 'meta', tag: 'meta',
nameAttribute: 'property', keyAttribute: 'property'
}, },
twitter: { twitter: {
group: true, group: true,
namespacedAttribute: true, namespacedAttribute: true,
tag: 'meta', tag: 'meta'
}, }
} }
export { defaultMapping } export { defaultMapping }
export function hasConfig(name: string): boolean { export function hasConfig (name: string): boolean {
return !!tags[name] || !!defaultMapping[name] return !!tags[name] || !!defaultMapping[name]
} }
export function getConfigKey( export function getConfigKey (
name: string | Array<string>, tagOrName: string | Array<string>,
key: string, key: string,
config: TODO, config: TODO
dontLog?: boolean
): any { ): any {
if (!dontLog) {
// console.log('getConfigKey', name, key, getConfigKey(name, key, config, true), config)
}
if (config && key in config) { if (config && key in config) {
return config[key] return config[key]
} }
if (isArray(name)) { if (isArray(tagOrName)) {
for (const _name of name) { for (const name of tagOrName) {
if (_name && _name in tags) { if (name && name in tags) {
return tags[_name][key] return tags[name][key]
} }
} }
return return
} }
if (name in tags) { if (tagOrName in tags) {
const tag = tags[name] const tag = tags[tagOrName]
return tag[key] return tag[key]
} }
} }
+21 -15
View File
@@ -1,22 +1,28 @@
/*
* This is a fixed config for real HTML tags
*
* TODO: we probably dont need all attributes
*/
export interface TagConfig { export interface TagConfig {
nameAttribute?: string keyAttribute?: string
contentAttributes: boolean | Array<string> attributes: boolean | Array<string>
[key: string]: any [key: string]: any
} }
const tags: { [key: string]: TagConfig } = { const tags: { [key: string]: TagConfig } = {
title: { title: {
contentAttributes: false, attributes: false
}, },
base: { base: {
contentAttributes: ['href', 'target'], attributes: ['href', 'target']
}, },
meta: { meta: {
nameAttribute: 'name', keyAttribute: 'name',
contentAttributes: ['content', 'name', 'http-equiv', 'charset'], attributes: ['content', 'name', 'http-equiv', 'charset']
}, },
link: { link: {
contentAttributes: [ attributes: [
'href', 'href',
'crossorigin', 'crossorigin',
'rel', 'rel',
@@ -29,14 +35,14 @@ const tags: { [key: string]: TagConfig } = {
'imagesrcset', 'imagesrcset',
'imagesizes', 'imagesizes',
'as', 'as',
'color', 'color'
], ]
}, },
style: { style: {
contentAttributes: ['media'], attributes: ['media']
}, },
script: { script: {
contentAttributes: [ attributes: [
'src', 'src',
'type', 'type',
'nomodule', 'nomodule',
@@ -44,12 +50,12 @@ const tags: { [key: string]: TagConfig } = {
'defer', 'defer',
'crossorigin', 'crossorigin',
'integrity', 'integrity',
'referrerpolicy', 'referrerpolicy'
], ]
}, },
noscript: { noscript: {
contentAttributes: false, attributes: false
}, }
} }
export { tags } export { tags }
+1 -1
View File
@@ -1,2 +1,2 @@
// Global compile-time constants // Global compile-time constants
declare var __DEV__: boolean declare let __DEV__: boolean
+2 -2
View File
@@ -1,6 +1,6 @@
import { setByObject } from './set'
import { MetaContext } from '../types' import { MetaContext } from '../types'
import { setByObject } from './set'
export function remove(context: MetaContext) { export function remove (context: MetaContext) {
setByObject(context, {}) setByObject(context, {})
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import { hasOwn } from '@vue/shared'
import { clone } from '../utils' import { clone } from '../utils'
import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types' import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types'
export function resolveActive( export function resolveActive (
context: MetaContext, context: MetaContext,
key: string, key: string,
pathSegments: PathSegments, pathSegments: PathSegments,
+6 -6
View File
@@ -1,9 +1,9 @@
import { isPlainObject, hasOwn } from '@vue/shared' import { isPlainObject, hasOwn } from '@vue/shared'
import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types'
import { shadow, active } from './globals' import { shadow, active } from './globals'
import { resolveActive } from './resolve' import { resolveActive } from './resolve'
import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types'
export function set( export function set (
context: MetaContext, context: MetaContext,
key: string, key: string,
value: any, value: any,
@@ -54,7 +54,7 @@ export function set(
resolveActive(context, key, pathSegments, shadowParent, activeParent) resolveActive(context, key, pathSegments, shadowParent, activeParent)
} }
export function setByObject( export function setByObject (
context: MetaContext, context: MetaContext,
value: any, value: any,
shadowParent: ShadowNode = shadow, shadowParent: ShadowNode = shadow,
@@ -70,14 +70,14 @@ export function setByObject(
if (isPlainObject(shadowParent[key])) { if (isPlainObject(shadowParent[key])) {
setByObject(context, {}, shadowParent[key], activeParent[key], [ setByObject(context, {}, shadowParent[key], activeParent[key], [
...pathSegments, ...pathSegments,
key, key
]) ])
continue continue
} }
set(context, key, undefined, shadowParent, activeParent, [ set(context, key, undefined, shadowParent, activeParent, [
...pathSegments, ...pathSegments,
key, key
]) ])
} }
@@ -85,7 +85,7 @@ export function setByObject(
for (const key in value) { for (const key in value) {
set(context, key, value[key], shadowParent, activeParent, [ set(context, key, value[key], shadowParent, activeParent, [
...pathSegments, ...pathSegments,
key, key
]) ])
} }
} }
+2 -2
View File
@@ -1,8 +1,8 @@
import { MetaContext, PathSegments, ShadowNode, ActiveNode } from '../types'
import { shadow, active } from './globals' import { shadow, active } from './globals'
import { set } from './set' import { set } from './set'
import { MetaContext, PathSegments, ShadowNode, ActiveNode } from '../types'
export function update( export function update (
context: MetaContext, context: MetaContext,
pathSegments: PathSegments, pathSegments: PathSegments,
key: string, key: string,
+1 -1
View File
@@ -10,7 +10,7 @@ declare module '@vue/runtime-core' {
} }
} }
export function applyMetaPlugin(app: App, manager: Manager) { export function applyMetaPlugin (app: App, manager: Manager) {
app.component('Metainfo', Metainfo) app.component('Metainfo', Metainfo)
app.config.globalProperties.$metaManager = manager app.config.globalProperties.$metaManager = manager
+7 -7
View File
@@ -16,21 +16,21 @@ export type Manager = {
install(app: App): void install(app: App): void
} }
export function createManager(options: TODO = {}): Manager { export function createManager (options: TODO = {}): Manager {
const { resolver = deepestResolver } = options const { resolver = deepestResolver } = options
// TODO: validate resolver // TODO: validate resolver
const manager: Manager = { const manager: Manager = {
resolver: { resolver: {
setup(context: MetaContext) { setup (context: MetaContext) {
if (!resolver || isFunction(resolver)) { if (!resolver || isFunction(resolver)) {
return return
} }
resolver.setup(context) resolver.setup(context)
}, },
resolve(key: string, pathSegments: PathSegments, getShadow, getActive) { resolve (key: string, pathSegments: PathSegments, getShadow, getActive) {
if (!resolver) { if (!resolver) {
return return
} }
@@ -40,15 +40,15 @@ export function createManager(options: TODO = {}): Manager {
} }
return resolver.resolve(key, pathSegments, getShadow, getActive) return resolver.resolve(key, pathSegments, getShadow, getActive)
}, }
}, },
config: { config: {
...defaultMapping, ...defaultMapping,
...options.config, ...options.config
}, },
install(app: App) { install (app: App) {
applyMetaPlugin(app, this) applyMetaPlugin(app, this)
}, }
} }
return manager return manager
+9 -10
View File
@@ -4,22 +4,22 @@ import { update } from './info/update'
import { MetaContext, MetainfoInput, PathSegments } from './types' import { MetaContext, MetainfoInput, PathSegments } from './types'
interface Target extends MetainfoInput { interface Target extends MetainfoInput {
__vm_proxy?: any __vm_proxy?: any // eslint-disable-line camelcase
} }
export function createProxy( export function createProxy (
target: Target, target: Target,
handler: ProxyHandler<object> handler: ProxyHandler<object>
): Target { ): Target {
return markRaw(new Proxy(target, handler)) return markRaw(new Proxy(target, handler))
} }
export function createHandler( export function createHandler (
context: MetaContext, context: MetaContext,
pathSegments: PathSegments = [] pathSegments: PathSegments = []
): ProxyHandler<object> { ): ProxyHandler<object> {
return { return {
get(target: object, key: string, receiver: object) { get (target: object, key: string, receiver: object) {
const value = Reflect.get(target, key, receiver) const value = Reflect.get(target, key, receiver)
if (!isObject(value)) { if (!isObject(value)) {
@@ -29,20 +29,19 @@ export function createHandler(
if (!value.__vm_proxy) { if (!value.__vm_proxy) {
const keyPath: PathSegments = [...pathSegments, key] const keyPath: PathSegments = [...pathSegments, key]
const handler = /*#__PURE__*/ createHandler(context, keyPath) const handler = /* #__PURE__ */ createHandler(context, keyPath)
value.__vm_proxy = createProxy(value, handler) value.__vm_proxy = createProxy(value, handler)
} }
return value.__vm_proxy return value.__vm_proxy
}, },
set( set (
target: object, target: object, // eslint-disable-line @typescript-eslint/no-unused-vars
key: string, key: string,
value: unknown, value: unknown
receiver: object
): boolean { ): boolean {
update(context, pathSegments, key, value) update(context, pathSegments, key, value)
return true return true
}, }
} }
} }
+52 -52
View File
@@ -24,18 +24,18 @@ export interface SlotScopeProperties {
export type RenderedMetainfoNode = { export type RenderedMetainfoNode = {
vnode: VNode vnode: VNode
target?: string to?: string
} }
export type RenderedMetainfo = Array<RenderedMetainfoNode> export type RenderedMetainfo = Array<RenderedMetainfoNode>
export function renderMeta( export function renderMeta (
context: RenderContext, context: RenderContext,
key: string, key: string,
data: TODO, data: TODO,
config: TODO config: TODO
): RenderedMetainfo | RenderedMetainfoNode { ): RenderedMetainfo | RenderedMetainfoNode {
// console.info('renderMeta', key, data, config) console.info('renderMeta', key, data, config)
if (config.group) { if (config.group) {
return renderGroup(context, key, data, config) return renderGroup(context, key, data, config)
@@ -44,33 +44,32 @@ export function renderMeta(
return renderTag(context, key, data, config) return renderTag(context, key, data, config)
} }
export function renderGroup( export function renderGroup (
context: RenderContext, context: RenderContext,
key: string, key: string,
data: TODO, data: TODO,
config: TODO config: TODO
): RenderedMetainfo | RenderedMetainfoNode { ): RenderedMetainfo | RenderedMetainfoNode {
// console.info('renderGroup', key, data, config) console.info('renderGroup', key, data, config)
if (isArray(data)) { if (isArray(data)) {
config.contentAttributes = getConfigKey( if (__DEV__) {
[key, config.tag], // eslint-disable-next-line no-console
'contentAttributes', console.warn('Specifying an array for group properties isnt supported as we didnt found a use-case for this yet. If you have one, please create an issue on the vue-meta repo')
config }
) // config.attributes = getConfigKey([key, config.tag], 'attributes', config)
return data.map(_data => renderTag(context, key, _data, config)).flat() return []
} }
return Object.keys(data) return Object.keys(data)
.map(childKey => { .map((childKey) => {
const groupConfig: GroupConfig = { const groupConfig: GroupConfig = {
group: key, group: key,
data, data
} }
if (config.namespaced) { if (config.namespaced) {
groupConfig.tagNamespace = groupConfig.tagNamespace = config.namespaced === true ? key : config.namespaced
config.namespaced === true ? key : config.namespaced
} else if (config.namespacedAttribute) { } else if (config.namespacedAttribute) {
const namespace = const namespace =
config.namespacedAttribute === true ? key : config.namespacedAttribute config.namespacedAttribute === true ? key : config.namespacedAttribute
@@ -84,31 +83,37 @@ export function renderGroup(
.flat() .flat()
} }
export function renderTag( export function renderTag (
context: RenderContext, context: RenderContext,
key: string, key: string,
data: TODO, data: TODO,
config: TODO = {}, config: TODO = {},
groupConfig?: GroupConfig groupConfig?: GroupConfig
): RenderedMetainfo | RenderedMetainfoNode { ): RenderedMetainfo | RenderedMetainfoNode {
console.info('renderTag', key, data, config, groupConfig)
/* TODO: not needed I think
if (!config.group && isArray(data)) { if (!config.group && isArray(data)) {
data = { content: data } data = { children: data }
} } */
let content, hasChilds let content, hasChilds
if (isArray(data)) { if (isArray(data)) {
return data return data
.map(child => { .map((child) => {
return renderTag(context, key, child, config, groupConfig) return renderTag(context, key, child, config, groupConfig)
}) })
.flat() .flat()
} else if (data.content && isArray(data.content)) { } else if (data.children && isArray(data.children)) {
content = data.content.map((child: string | TODO) => { content = data.children.map((child: string | TODO) => {
if (typeof child === 'string') { const data = renderTag(context, key, child, config, groupConfig)
return child
if (isArray(data)) {
return data.map(({ vnode }) => vnode)
} }
return renderTag(context, key, child, config, groupConfig)
return data.vnode
}) })
hasChilds = true hasChilds = true
} else { } else {
@@ -125,36 +130,35 @@ export function renderTag(
attributes = { ...data } attributes = { ...data }
delete attributes.tag delete attributes.tag
delete attributes.content delete attributes.children
delete attributes.target delete attributes.to
} else { } else {
attributes = {} attributes = {}
} }
if (hasChilds) { if (hasChilds) {
content = getSlotContent(context, slotName, content, config, data) content = getSlotContent(context, slotName, content, data)
} else { } else {
const contentAttributes = getConfigKey(tag, 'contentAttributes', config) const tagAttributes = getConfigKey([tag, config.tag], 'attributes', config)
console.log('tagAttributes', tagAttributes, config, tag)
if (contentAttributes) { if (tagAttributes) {
if (!config.nameless) { if (!config.nameless) {
const nameAttribute = getConfigKey(tag, 'nameAttribute', config) const keyAttribute = getConfigKey([tag, config.tag], 'keyAttribute', config)
if (nameAttribute) { if (keyAttribute) {
attributes[nameAttribute] = fullName attributes[keyAttribute] = fullName
} }
} }
const contentAttribute = config.contentAttribute || contentAttributes[0] const valueAttribute = config.valueAttribute || tagAttributes[0]
attributes[contentAttribute] = getSlotContent( attributes[valueAttribute] = getSlotContent(
context, context,
slotName, slotName,
attributes[contentAttribute] || content, attributes[valueAttribute] || content,
config,
groupConfig groupConfig
) )
content = undefined content = undefined
} else { } else {
content = getSlotContent(context, slotName, content, config, data) content = getSlotContent(context, slotName, content, data)
} }
} }
@@ -170,11 +174,8 @@ export function renderTag(
if (hasChilds) { if (hasChilds) {
for (const child of content) { for (const child of content) {
if (typeof child === 'string') {
continue
}
if (child.type === finalTag) { if (child.type === finalTag) {
// TODO: what was this about again?!?!?!?!
return content return content
} }
@@ -185,36 +186,35 @@ export function renderTag(
const vnode = h(finalTag, attributes, content) const vnode = h(finalTag, attributes, content)
return { return {
target: data.target, to: data.to,
vnode, vnode
} }
} }
export function getSlotContent( export function getSlotContent (
{ metainfo, slots }: RenderContext, { metainfo, slots }: RenderContext,
slotName: string, slotName: string,
content: any, content: any,
config: TODO,
groupConfig?: GroupConfig groupConfig?: GroupConfig
): TODO { ): TODO {
if (!slots[slotName]) { if (!slots || !slots[slotName]) {
return content return content
} }
const slotProps: SlotScopeProperties = { const slotProps: SlotScopeProperties = {
content, content,
metainfo, metainfo
} }
if (groupConfig && groupConfig.group) { if (groupConfig && groupConfig.group) {
slotProps[groupConfig.group] = groupConfig.data slotProps[groupConfig.group] = groupConfig.data
} }
content = slots[slotName](slotProps) const slotContent = slots[slotName](slotProps)
if (content.length) { if (slotContent && slotContent.length) {
return content[0].children return slotContent[0].children
} }
return '' return content
} }
+4 -4
View File
@@ -1,13 +1,13 @@
import { import {
ActiveNode, ActiveNode,
/*ActiveResolverSetup, ActiveResolverMethod,*/ MetaContext, /* ActiveResolverSetup, ActiveResolverMethod, */ MetaContext,
PathSegments, PathSegments,
ShadowNode, ShadowNode
} from '../types' } from '../types'
export function setup(context: MetaContext): void {} export function setup (context: MetaContext): void {}
export function resolve( export function resolve (
key: string, key: string,
pathSegments: PathSegments, pathSegments: PathSegments,
shadow: ShadowNode, shadow: ShadowNode,
+7 -8
View File
@@ -7,7 +7,7 @@ import { MetaContext, MetainfoActive, MetainfoInput } from './types'
let contextCounter: number = 0 let contextCounter: number = 0
export function useMeta(obj: MetainfoInput, manager?: Manager) { export function useMeta (obj: MetainfoInput, manager?: Manager) {
const vm = getCurrentInstance() const vm = getCurrentInstance()
if (vm) { if (vm) {
console.log(vm) console.log(vm)
@@ -17,16 +17,15 @@ export function useMeta(obj: MetainfoInput, manager?: Manager) {
if (!manager) { if (!manager) {
// oopsydoopsy // oopsydoopsy
throw new Error('No manager or current instance') throw new Error('No manager or current instance')
return
} }
const context: MetaContext = { const context: MetaContext = {
id: PolySymbol(`context ${contextCounter++}`), id: PolySymbol(`context ${contextCounter++}`),
vm, vm,
manager, manager
} }
let unmount = <T extends Function = () => any>() => remove(context) const unmount = <T extends Function = () => any>() => remove(context)
if (vm) { if (vm) {
onUnmounted(unmount) onUnmounted(unmount)
} }
@@ -37,20 +36,20 @@ export function useMeta(obj: MetainfoInput, manager?: Manager) {
setByObject(context, obj) setByObject(context, obj)
const handler = /*#__PURE__*/ createHandler(context) const handler = /* #__PURE__ */ createHandler(context)
const meta = createProxy(obj, handler) const meta = createProxy(obj, handler)
return { return {
meta, meta,
unmount, unmount
} }
} }
export function useMetainfo(): MetainfoActive { export function useMetainfo (): MetainfoActive {
return inject(metaInfoKey)! return inject(metaInfoKey)!
} }
export function getCurrentManager(): Manager { export function getCurrentManager (): Manager {
const vm = getCurrentInstance()! const vm = getCurrentInstance()!
return vm.appContext.config.globalProperties.$metaManager return vm.appContext.config.globalProperties.$metaManager
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import { isArray, isObject } from '@vue/shared' import { isArray, isObject } from '@vue/shared'
// See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts // See: https://github.com/vuejs/vue-next/blob/08b4e8815da4e8911058ccbab986bea6365c3352/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
export function clone(v: any): any { export function clone (v: any): any {
if (isArray(v)) { if (isArray(v)) {
return v.map(clone) return v.map(clone)
} }
+4
View File
@@ -0,0 +1,4 @@
process.server = true
jest.useFakeTimers()
jest.setTimeout(30000)
+432
View File
@@ -0,0 +1,432 @@
import * as render from '../../src/render'
// Note: testing isnt rly independent as they also rely on ./src/config/tags
describe('render', () => {
test('render key-string element (without value attribute)', () => {
const context = {}
const key = 'TitleTest'
const data = 'my title'
const config = {
tag: 'title'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res)
expect(res.to).toBeUndefined()
expect(res.vnode).toMatchObject({ __v_isVNode: true })
expect(res.vnode.type).toEqual('title')
expect(res.vnode.props).toEqual({})
expect(res.vnode.children).toEqual('my title')
})
test('render key-string element (without name attribute)', () => {
const context = {}
const key = 'CharsetTest'
const data = 'utf8'
const config = {
tag: 'meta',
nameless: true,
valueAttribute: 'charset'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res)
expect(res.to).toBeUndefined()
expect(res.vnode).toMatchObject({ __v_isVNode: true })
expect(res.vnode.type).toEqual('meta')
expect(res.vnode.props).toMatchObject({ charset: 'utf8' })
})
test('render key-string element (with name attribute)', () => {
const context = {}
const key = 'DescriptionTest'
const data = 'my description'
const config = {
tag: 'meta'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res)
expect(res.to).toBeUndefined()
expect(res.vnode).toMatchObject({ __v_isVNode: true })
expect(res.vnode.type).toEqual('meta')
expect(res.vnode.props).toMatchObject({ name: 'DescriptionTest', content: 'my description' })
})
test('render key-object element', () => {
const context = {}
const key = 'DescriptionTest2'
const data = { content: 'my description 2' }
const config = {
tag: 'meta'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res)
expect(res.to).toBeUndefined()
expect(res.vnode).toMatchObject({ __v_isVNode: true })
expect(res.vnode.type).toEqual('meta')
expect(res.vnode.props).toMatchObject({ name: 'DescriptionTest2', content: 'my description 2' })
})
test('render array<key-string> elements', () => {
const context = {}
const key = 'kal-el'
const data = [
'man',
'woman'
]
const config = {
tag: 'meta',
valueAttribute: 'super'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res)
expect(res).toBeInstanceOf(Array)
expect(res.length).toBe(2)
expect(res[0].to).toBeUndefined()
expect(res[0].vnode).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.type).toEqual('meta')
expect(res[0].vnode.props).toMatchObject({ super: 'man' })
expect(res[1].to).toBeUndefined()
expect(res[1].vnode).toMatchObject({ __v_isVNode: true })
expect(res[1].vnode.type).toEqual('meta')
expect(res[1].vnode.props).toMatchObject({ super: 'woman' })
})
test('render array<key-object> elements', () => {
const context = {}
const key = 'kal-el'
const data = [
{ super: 'man' },
{ clark: 'kent' }
]
const config = {}
const res = render.renderMeta(context, key, data, config)
// console.log(res)
expect(res).toBeInstanceOf(Array)
expect(res.length).toBe(2)
expect(res[0].to).toBeUndefined()
expect(res[0].vnode).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.type).toEqual('kal-el')
expect(res[0].vnode.props).toMatchObject({ super: 'man' })
expect(res[1].to).toBeUndefined()
expect(res[1].vnode).toMatchObject({ __v_isVNode: true })
expect(res[1].vnode.type).toEqual('kal-el')
expect(res[1].vnode.props).toMatchObject({ clark: 'kent' })
})
test('render custom group', () => {
const context = {}
const key = 'customGroup'
const data = {
title: 'my custom title',
description: 'my custom description'
}
const config = {
group: true,
tag: 'meta'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res, res[0])
expect(res).toBeInstanceOf(Array)
expect(res.length).toBe(2)
expect(res[0].to).toBeUndefined()
expect(res[0].vnode).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.type).toEqual('meta')
expect(res[0].vnode.props).toMatchObject({ content: 'my custom title' })
expect(res[1].to).toBeUndefined()
expect(res[1].vnode).toMatchObject({ __v_isVNode: true })
expect(res[1].vnode.type).toEqual('meta')
expect(res[1].vnode.props).toMatchObject({ content: 'my custom description' })
})
test('render custom group (namespaced tag)', () => {
const context = {}
const key = 'og'
const data = {
title: 'my og title',
description: 'my og description',
image: [
'img1',
'img2'
]
}
const config = {
group: true,
keyAttribute: 'property',
namespacedAttribute: true,
tag: 'meta'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res, res[0])
expect(res).toBeInstanceOf(Array)
expect(res.length).toBe(4)
expect(res[0].to).toBeUndefined()
expect(res[0].vnode).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.type).toEqual('meta')
expect(res[0].vnode.props).toMatchObject({ property: 'og:title', content: 'my og title' })
expect(res[1].to).toBeUndefined()
expect(res[1].vnode).toMatchObject({ __v_isVNode: true })
expect(res[1].vnode.type).toEqual('meta')
expect(res[1].vnode.props).toMatchObject({ property: 'og:description', content: 'my og description' })
expect(res[2].to).toBeUndefined()
expect(res[2].vnode).toMatchObject({ __v_isVNode: true })
expect(res[2].vnode.type).toEqual('meta')
expect(res[2].vnode.props).toMatchObject({ property: 'og:image', content: 'img1' })
expect(res[3].to).toBeUndefined()
expect(res[3].vnode).toMatchObject({ __v_isVNode: true })
expect(res[3].vnode.type).toEqual('meta')
expect(res[3].vnode.props).toMatchObject({ property: 'og:image', content: 'img2' })
})
test('render custom group (namespaced attribute and name attribute)', () => {
const context = {}
const key = 'og'
const data = {
title: 'my og title',
description: 'my og description',
image: [
'img1',
'img2'
]
}
const config = {
group: true,
keyAttribute: 'property',
namespacedAttribute: true,
tag: 'meta'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res, res[0])
expect(res).toBeInstanceOf(Array)
expect(res.length).toBe(4)
expect(res[0].to).toBeUndefined()
expect(res[0].vnode).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.type).toEqual('meta')
expect(res[0].vnode.props).toMatchObject({ property: 'og:title', content: 'my og title' })
expect(res[1].to).toBeUndefined()
expect(res[1].vnode).toMatchObject({ __v_isVNode: true })
expect(res[1].vnode.type).toEqual('meta')
expect(res[1].vnode.props).toMatchObject({ property: 'og:description', content: 'my og description' })
expect(res[2].to).toBeUndefined()
expect(res[2].vnode).toMatchObject({ __v_isVNode: true })
expect(res[2].vnode.type).toEqual('meta')
expect(res[2].vnode.props).toMatchObject({ property: 'og:image', content: 'img1' })
expect(res[3].to).toBeUndefined()
expect(res[3].vnode).toMatchObject({ __v_isVNode: true })
expect(res[3].vnode.type).toEqual('meta')
expect(res[3].vnode.props).toMatchObject({ property: 'og:image', content: 'img2' })
})
test('render custom group (array)', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(_ => _)
const context = {}
const key = 'og'
const data = ['data']
const config = {
group: true,
keyAttribute: 'property',
namespacedAttribute: true,
tag: 'meta'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res, res[0])
expect(res).toBeInstanceOf(Array)
expect(res.length).toBe(0)
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(expect.stringContaining('Specifying an array for group properties isnt supported'))
spy.mockRestore()
})
test('render custom group (tag namespaced)', () => {
const context = {}
const key = 'esi'
const data = {
children: [{
tag: 'choose',
children: [{
tag: 'when',
test: '$(HTTP_COOKIE{group})=="Advanced"',
children: [{
tag: 'include',
src: 'http://www.example.com/advanced.html'
}]
}]
}]
}
const config = {
group: true,
namespaced: true,
attributes: ['src', 'test', 'text']
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res)
expect(res).toBeInstanceOf(Array)
expect(res.length).toBe(1)
expect(res[0].to).toBeUndefined()
expect(res[0].vnode).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.type).toEqual('esi:choose')
expect(res[0].vnode.props).toEqual({})
expect(res[0].vnode.children).toBeInstanceOf(Array)
expect(res[0].vnode.children.length).toBe(1)
expect(res[0].vnode.children[0]).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.children[0].type).toEqual('esi:when')
expect(res[0].vnode.children[0].props).toEqual({ test: '$(HTTP_COOKIE{group})=="Advanced"' })
expect(res[0].vnode.children[0].children).toBeInstanceOf(Array)
expect(res[0].vnode.children[0].children.length).toBe(1)
expect(res[0].vnode.children[0].children[0]).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.children[0].children[0].type).toEqual('esi:include')
expect(res[0].vnode.children[0].children[0].props).toEqual({ src: 'http://www.example.com/advanced.html' })
})
test('customized render with slot', () => {
const key = 'title'
const data = 'my title'
const config = {
tag: 'title'
}
const slot = jest.fn().mockReturnValue([{ children: 'slot title' }])
const context = {
metainfo: { [key]: data },
slots: { title: slot }
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res)
expect(res.to).toBeUndefined()
expect(res.vnode).toMatchObject({ __v_isVNode: true })
expect(res.vnode.type).toEqual('title')
expect(res.vnode.props).toEqual({})
expect(res.vnode.children).toEqual('slot title')
expect(slot).toHaveBeenCalledTimes(1)
expect(slot).toHaveBeenCalledWith({
content: data,
metainfo: context.metainfo
})
})
test('customized render with slot (group)', () => {
const key = 'og'
const data = {
title: 'my og title'
}
const slot = jest.fn().mockReturnValue([{ children: 'og slot title' }])
const context = {
metainfo: { [key]: data },
slots: { 'og(title)': slot }
}
const config = {
group: true,
keyAttribute: 'property',
namespacedAttribute: true,
tag: 'meta'
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res, res[0])
expect(res).toBeInstanceOf(Array)
expect(res.length).toBe(1)
expect(res[0].to).toBeUndefined()
expect(res[0].vnode).toMatchObject({ __v_isVNode: true })
expect(res[0].vnode.type).toEqual('meta')
expect(res[0].vnode.props).toMatchObject({ property: 'og:title', content: 'og slot title' })
expect(slot).toHaveBeenCalledTimes(1)
expect(slot).toHaveBeenCalledWith({
content: data.title,
[key]: data,
metainfo: context.metainfo
})
})
test('customized render with slot (fallsback to original content)', () => {
const key = 'title'
const data = 'my title'
const config = {
tag: 'title'
}
const slot = jest.fn().mockReturnValue(undefined) // slot returns nothing
const context = {
metainfo: { [key]: data },
slots: { title: slot }
}
const res = render.renderMeta(context, key, data, config)
// console.log('RES', res)
expect(res.to).toBeUndefined()
expect(res.vnode).toMatchObject({ __v_isVNode: true })
expect(res.vnode.type).toEqual('title')
expect(res.vnode.props).toEqual({})
expect(res.vnode.children).toEqual('my title')
expect(slot).toHaveBeenCalledTimes(1)
expect(slot).toHaveBeenCalledWith({
content: data,
metainfo: context.metainfo
})
})
})
-4
View File
@@ -1,4 +0,0 @@
process.server = true
jest.useFakeTimers()
jest.setTimeout(30000)
-98
View File
@@ -1,98 +0,0 @@
const fs = require('fs')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// https://github.com/jantimon/html-webpack-plugin/issues/1372
const HtmlIncludeChunksWebpackPlugin = require('@wishy-gift/html-include-chunks-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const { VueLoaderPlugin } = require('vue-loader')
const webpack = require('webpack')
const r = (...paths) => path.resolve(__dirname, 'examples', ...paths)
/** @type {import('webpack').ConfigurationFactory} */
const config = (env = {}) => {
const extraPlugins = env.prod ? [new BundleAnalyzerPlugin()] : []
return {
mode: env.prod ? 'production' : 'development',
devtool: env.prod ? 'source-map' : 'inline-source-map',
devServer: {
contentBase: r(),
historyApiFallback: true,
hot: true,
stats: 'minimal',
},
output: {
path: r('/__build__/'),
publicPath: '/__build__/',
filename: '[name].js',
},
entry: fs.readdirSync(r())
.reduce((entries, entryPath) => {
if (entryPath !== 'vue-router') {
return entries
}
if (entryPath === 'ssr') {
entries[entryPath] = r(entryPath, 'browser.js')
} else {
const entry = r(entryPath, 'app.js')
if (fs.existsSync(entry)) {
entries[entryPath] = entry
}
}
extraPlugins.push(
new HtmlWebpackPlugin({
entryKey: entryPath,
filename: r(entryPath, 'index.html')
})
)
return entries
}, {}),
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
},
{
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/runtime-dom',
vue: 'vue/dist/vue.esm-bundler.js',
},
// Add `.ts` and `.tsx` as a resolvable extension.
extensions: ['.ts', 'd.ts', '.tsx', '.js', '.vue'],
},
plugins: [
new VueLoaderPlugin(),
/*new HtmlWebpackPlugin({
template: r(__dirname, 'examples/index.html'),
}),*/
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!env.prod),
__BROWSER__: 'true',
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
}),
...extraPlugins,
],
}
}
module.exports = config
+1820 -4557
View File
File diff suppressed because it is too large Load Diff