mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-11 19:52:24 +03:00
feat: enable onload callbacks (#414)
* refactor(examples): run ssr example from server * chore: switch to babel for build buble complains too much * feat: enable loaded callbacks feat: add skip option * examples: add async-callback browser example * examples: fix server * examples(ssr): add reactive script with callback * fix: also skip on ssr * chore: remove unused var * feat: only add mutationobserver if DOM is still loading feat: disconnect mutation observer once DOM has loaded * examples: pass vmid to loadCallback instead of el * feat: also support load callbacks for link/style tags * test: add unit tests for load * test: add load e2e test * chore: fix lint * chore: remove unused files * test: fix e2e load callback test * test: fix attempt * examples: ie9 compatiblity destructuring doesnt work in ie9 * fix: add onload attribute on ssr dont rely on mutationobserver * chore: lint ci conf * refactor: remove loadCallbackAttribute config option test: fix coverage for load * test: improve coverage * fix: only use console when it exists (for ie9) * chore: fix coverage
This commit is contained in:
@@ -134,5 +134,3 @@ workflows:
|
||||
requires: [test-e2e-ssr]
|
||||
filters:
|
||||
branches: { ignore: /^pull\/.*/ }
|
||||
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
<li><a href="basic-render">Basic Render</a></li>
|
||||
<li><a href="keep-alive">Keep alive</a></li>
|
||||
<li><a href="multiple-apps">Usage with multiple apps</a></li>
|
||||
<li><a href="ssr">SSR</a></li>
|
||||
<li><a href="vue-router">Usage with vue-router</a></li>
|
||||
<li><a href="vuex">Usage with vuex</a></li>
|
||||
<li><a href="vuex-async">Usage with vuex + async actions</a></li>
|
||||
<li><a href="async-callback">Async Callback</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
window.users.push({
|
||||
'id': 1,
|
||||
'name': 'Leanne Graham',
|
||||
'username': 'Bret',
|
||||
'email': 'Sincere@april.biz',
|
||||
'address': {
|
||||
'street': 'Kulas Light',
|
||||
'suite': 'Apt. 556',
|
||||
'city': 'Gwenborough',
|
||||
'zipcode': '92998-3874',
|
||||
'geo': {
|
||||
'lat': '-37.3159',
|
||||
'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'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
window.users.push({
|
||||
'id': 2,
|
||||
'name': 'Ervin Howell',
|
||||
'username': 'Antonette',
|
||||
'email': 'Shanna@melissa.tv',
|
||||
'address': {
|
||||
'street': 'Victor Plains',
|
||||
'suite': 'Suite 879',
|
||||
'city': 'Wisokyburgh',
|
||||
'zipcode': '90566-7771',
|
||||
'geo': {
|
||||
'lat': '-43.9509',
|
||||
'lng': '-34.4618'
|
||||
}
|
||||
},
|
||||
'phone': '010-692-6593 x09125',
|
||||
'website': 'anastasia.net',
|
||||
'company': {
|
||||
'name': 'Deckow-Crist',
|
||||
'catchPhrase': 'Proactive didactic contingency',
|
||||
'bs': 'synergize scalable supply-chains'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
window.users.push({
|
||||
'id': 3,
|
||||
'name': 'Clementine Bauch',
|
||||
'username': 'Samantha',
|
||||
'email': 'Nathan@yesenia.net',
|
||||
'address': {
|
||||
'street': 'Douglas Extension',
|
||||
'suite': 'Suite 847',
|
||||
'city': 'McKenziehaven',
|
||||
'zipcode': '59590-4157',
|
||||
'geo': {
|
||||
'lat': '-68.6102',
|
||||
'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'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
window.users.push({
|
||||
'id': 4,
|
||||
'name': 'Patricia Lebsack',
|
||||
'username': 'Karianne',
|
||||
'email': 'Julianne.OConner@kory.org',
|
||||
'address': {
|
||||
'street': 'Hoeger Mall',
|
||||
'suite': 'Apt. 692',
|
||||
'city': 'South Elvis',
|
||||
'zipcode': '53919-4257',
|
||||
'geo': {
|
||||
'lat': '29.4572',
|
||||
'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'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import Vue from 'vue'
|
||||
import VueMeta from 'vue-meta'
|
||||
|
||||
Vue.use(VueMeta)
|
||||
|
||||
window.users = []
|
||||
|
||||
new Vue({
|
||||
metaInfo () {
|
||||
return {
|
||||
title: 'Async Callback',
|
||||
titleTemplate: '%s | Vue Meta Examples',
|
||||
script: [
|
||||
{
|
||||
skip: this.count < 2,
|
||||
vmid: 'potatoes',
|
||||
src: '/user-3.js',
|
||||
async: true,
|
||||
callback: this.updateCounter
|
||||
},
|
||||
{
|
||||
skip: this.count < 1,
|
||||
vmid: 'vegetables',
|
||||
src: '/user-2.js',
|
||||
async: true,
|
||||
callback: this.updateCounter
|
||||
},
|
||||
{
|
||||
vmid: 'meat',
|
||||
src: '/user-1.js',
|
||||
async: true,
|
||||
callback: el => this.loadCallback(el.getAttribute('data-vmid'))
|
||||
},
|
||||
...this.scripts
|
||||
]
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
count: 0,
|
||||
scripts: [],
|
||||
users: window.users
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
count (val) {
|
||||
if (val === 3) {
|
||||
this.addScript()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateCounter () {
|
||||
this.count++
|
||||
},
|
||||
addScript () {
|
||||
this.scripts.push({
|
||||
src: '/user-4.js',
|
||||
callback: () => {
|
||||
this.updateCounter()
|
||||
}
|
||||
})
|
||||
},
|
||||
loadCallback (vmid) {
|
||||
if (vmid === 'meat') {
|
||||
this.updateCounter()
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div id="app">
|
||||
<h1>Async Callback</h1>
|
||||
<p>{{ count }} scripts loaded</p>
|
||||
|
||||
<div>
|
||||
<h2>Users</h2>
|
||||
<ul>
|
||||
<li
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
>
|
||||
<strong>{{ user.id }}</strong>: {{ user.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}).$mount('#app')
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Async Callback Title</title>
|
||||
<link rel="stylesheet" href="/global.css">
|
||||
</head>
|
||||
<body>
|
||||
<a href="/">← Examples index</a>
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="/__build__/async-callback.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,10 +11,10 @@ Vue.component('child', {
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
render (h) {
|
||||
return h('h3', null, this.page)
|
||||
},
|
||||
metaInfo() {
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.page
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ Vue.component('foo', {
|
||||
})
|
||||
|
||||
new Vue({
|
||||
data() {
|
||||
data () {
|
||||
return { showFoo: false }
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
show () {
|
||||
this.showFoo = !this.showFoo
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ Vue.use(VueMeta)
|
||||
// index.html contains a manual SSR render
|
||||
|
||||
const app1 = new Vue({
|
||||
metaInfo() {
|
||||
metaInfo () {
|
||||
return {
|
||||
title: 'App 1 title',
|
||||
bodyAttrs: {
|
||||
@@ -14,15 +14,15 @@ const app1 = new Vue({
|
||||
},
|
||||
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'
|
||||
}
|
||||
@@ -44,7 +44,7 @@ const app2 = new Vue({
|
||||
],
|
||||
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: `
|
||||
@@ -60,7 +60,6 @@ const app3 = new Vue({
|
||||
`
|
||||
}).$mount('#app3')
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('trigger app 1')
|
||||
app1.$data.ogContent = 'Hello from app 1'
|
||||
@@ -75,8 +74,9 @@ setTimeout(() => {
|
||||
console.log('trigger app 3')
|
||||
app3.$meta().refresh()
|
||||
}, 7500)
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('trigger app 4')
|
||||
const App = Vue.extend({ template: `<div>app 4</div>` })
|
||||
const app4 = new App().$mount()
|
||||
new App().$mount()
|
||||
}, 10000)
|
||||
|
||||
+18
-1
@@ -6,11 +6,13 @@ import rewrite from 'express-urlrewrite'
|
||||
import webpack from 'webpack'
|
||||
import webpackDevMiddleware from 'webpack-dev-middleware'
|
||||
import WebpackConfig from './webpack.config'
|
||||
import { renderPage } from './ssr/server'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(webpackDevMiddleware(webpack(WebpackConfig), {
|
||||
publicPath: '/__build__/',
|
||||
writeToDisk: false,
|
||||
stats: {
|
||||
colors: true,
|
||||
chunks: false
|
||||
@@ -21,12 +23,27 @@ fs.readdirSync(__dirname)
|
||||
.filter(file => file !== 'ssr')
|
||||
.forEach((file) => {
|
||||
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
|
||||
app.use(rewrite('/' + file + '/*', '/' + file + '/index.html'))
|
||||
app.use(rewrite(`/${file}/*`, `/${file}/index.html`))
|
||||
}
|
||||
})
|
||||
|
||||
app.use(express.static(path.join(__dirname, '_static')))
|
||||
app.use(express.static(__dirname))
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
if (!req.url.startsWith('/ssr')) {
|
||||
next()
|
||||
}
|
||||
|
||||
try {
|
||||
const html = await renderPage()
|
||||
res.send(html)
|
||||
} catch (e) {
|
||||
consola.error('SSR Oops:', e)
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
const host = process.env.HOST || 'localhost'
|
||||
const port = process.env.PORT || 3000
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import Vue from 'vue'
|
||||
import VueMeta from '../../'
|
||||
|
||||
Vue.use(VueMeta, {
|
||||
tagIDKeyName: 'hid'
|
||||
})
|
||||
|
||||
export default function createApp () {
|
||||
return new Vue({
|
||||
components: {
|
||||
Hello: {
|
||||
template: '<p>Hello World</p>',
|
||||
metaInfo: {
|
||||
title: 'Hello World',
|
||||
meta: [
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'The description'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: 'Boring Title',
|
||||
htmlAttrs: { amp: true },
|
||||
meta: [
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'Say something'
|
||||
}
|
||||
],
|
||||
script: [
|
||||
{
|
||||
hid: 'ldjson-schema',
|
||||
type: 'application/ld+json',
|
||||
innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }'
|
||||
}, {
|
||||
type: 'application/ld+json',
|
||||
innerHTML: '{ "body": "yes" }',
|
||||
body: true
|
||||
}, {
|
||||
hid: 'my-async-script-with-load-callback',
|
||||
src: '/user-1.js',
|
||||
body: true,
|
||||
defer: true,
|
||||
callback: this.loadCallback
|
||||
}, {
|
||||
skip: this.count < 1,
|
||||
src: '/user-2.js',
|
||||
body: true,
|
||||
callback: this.loadCallback
|
||||
}
|
||||
],
|
||||
__dangerouslyDisableSanitizersByTagID: {
|
||||
'ldjson-schema': ['innerHTML']
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
count: 0,
|
||||
users: process.server ? [] : window.users
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadCallback () {
|
||||
this.count++
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div id="app">
|
||||
<hello/>
|
||||
|
||||
<p>{{ count }} users loaded</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
>
|
||||
{{ user.id }}: {{ user.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>`
|
||||
})
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export default async function createApp() {
|
||||
// the dynamic import is for this example only
|
||||
const vueMetaModule = process.env.NODE_ENV === 'development' ? '../../' : 'vue-meta'
|
||||
const VueMeta = await import(vueMetaModule).then(m => m.default || m)
|
||||
|
||||
Vue.use(VueMeta, {
|
||||
tagIDKeyName: 'hid'
|
||||
})
|
||||
|
||||
return new Vue({
|
||||
components: {
|
||||
Hello: {
|
||||
template: '<p>Hello</p>',
|
||||
metaInfo: {
|
||||
title: 'Coucou',
|
||||
meta: [
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'Coucou'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<hello/>',
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
htmlAttrs: { amp: true },
|
||||
meta: [
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'Hello World'
|
||||
}
|
||||
],
|
||||
script: [
|
||||
{
|
||||
hid: 'ldjson-schema',
|
||||
type: 'application/ld+json',
|
||||
innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }'
|
||||
}, {
|
||||
type: 'application/ld+json',
|
||||
innerHTML: '{ "body": "yes" }',
|
||||
body: true
|
||||
}
|
||||
],
|
||||
__dangerouslyDisableSanitizersByTagID: {
|
||||
'ldjson-schema': ['innerHTML']
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
<!doctype html>
|
||||
<html data-vue-meta-server-rendered {{ htmlAttrs.text() }}>
|
||||
<html {{ htmlAttrs.text(true) }}>
|
||||
<head {{ headAttrs.text() }}>
|
||||
{{ meta.text() }}
|
||||
{{ title.text() }}
|
||||
{{ meta.text() }}
|
||||
<link rel="stylesheet" href="/global.css">
|
||||
{{ link.text() }}
|
||||
{{ style.text() }}
|
||||
{{ webpackAssets }}
|
||||
{{ script.text() }}
|
||||
{{ noscript.text() }}
|
||||
</head>
|
||||
<body {{ bodyAttrs.text() }}>
|
||||
{{ script.text({ pbody: true }) }}
|
||||
{{ noscript.text({ pbody: true }) }}
|
||||
|
||||
<a href="/">← Examples index</a>
|
||||
{{ app }}
|
||||
|
||||
<script src="/__build__/ssr.js"></script>
|
||||
{{ script.text({ body: true }) }}
|
||||
{{ noscript.text({ body: true }) }}
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import createApp from './App'
|
||||
|
||||
window.users = []
|
||||
|
||||
createApp().$mount('#app')
|
||||
@@ -1,3 +0,0 @@
|
||||
import createApp from './app'
|
||||
|
||||
createApp().$mount('#app')
|
||||
@@ -1,36 +0,0 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import template from 'lodash/template'
|
||||
import { createRenderer } from 'vue-server-renderer'
|
||||
import consola from 'consola'
|
||||
import createApp from './server-entry'
|
||||
|
||||
const renderer = createRenderer()
|
||||
|
||||
async function createPage() {
|
||||
const templateFile = path.resolve(__dirname, 'app.template.html')
|
||||
const templateContent = await fs.readFile(templateFile, { encoding: 'utf8' })
|
||||
|
||||
// see: https://lodash.com/docs#template
|
||||
const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
|
||||
|
||||
const webpackAssets = '<link rel="stylesheet" href="../global.css">'
|
||||
const serverApp = await createApp()
|
||||
const appHtml = await renderer.renderToString(serverApp)
|
||||
|
||||
const pageHtml = compiled({
|
||||
app: appHtml,
|
||||
webpackAssets,
|
||||
...serverApp.$meta().inject()
|
||||
})
|
||||
|
||||
return pageHtml
|
||||
}
|
||||
|
||||
consola.info(`Creating ssr page`)
|
||||
createPage()
|
||||
.then((pageHtml) => {
|
||||
consola.info(`Done, page:`)
|
||||
consola.log(pageHtml)
|
||||
})
|
||||
.catch(e => consola.error(e))
|
||||
@@ -1,3 +0,0 @@
|
||||
import createApp from './app'
|
||||
|
||||
export default createApp
|
||||
@@ -0,0 +1,27 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import template from 'lodash/template'
|
||||
import { createRenderer } from 'vue-server-renderer'
|
||||
import createApp from './App'
|
||||
|
||||
const renderer = createRenderer({ runInNewContext: false })
|
||||
|
||||
const templateFile = path.resolve(__dirname, 'app.template.html')
|
||||
const templateContent = fs.readFileSync(templateFile, { encoding: 'utf8' })
|
||||
|
||||
// see: https://lodash.com/docs#template
|
||||
const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
|
||||
|
||||
process.server = true
|
||||
|
||||
export async function renderPage () {
|
||||
const app = await createApp()
|
||||
const appHtml = await renderer.renderToString(app)
|
||||
|
||||
const pageHtml = compiled({
|
||||
app: appHtml,
|
||||
...app.$meta().inject()
|
||||
})
|
||||
|
||||
return pageHtml
|
||||
}
|
||||
@@ -15,21 +15,21 @@ const ChildComponent = {
|
||||
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
|
||||
<p>Has metaInfo been updated? {{ metaUpdated }}</p>
|
||||
</div>`,
|
||||
metaInfo() {
|
||||
metaInfo () {
|
||||
return {
|
||||
title: `${this.page} - ${this.date && this.date.toTimeString()}`,
|
||||
afterNavigation() {
|
||||
afterNavigation () {
|
||||
metaUpdated = 'yes'
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
date: null,
|
||||
metaUpdated
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mounted () {
|
||||
setInterval(() => {
|
||||
this.date = new Date()
|
||||
}, 1000)
|
||||
@@ -39,10 +39,10 @@ const ChildComponent = {
|
||||
// this wrapper function is not a requirement for vue-router,
|
||||
// just a demonstration that render-function style components also work.
|
||||
// See https://github.com/nuxt/vue-meta/issues/9 for more info.
|
||||
function view(page) {
|
||||
function view (page) {
|
||||
return {
|
||||
name: `section-${page}`,
|
||||
render(h) {
|
||||
render (h) {
|
||||
return h(ChildComponent, {
|
||||
props: { page }
|
||||
})
|
||||
|
||||
@@ -36,33 +36,33 @@ export default new Vuex.Store({
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -36,27 +36,27 @@ export default new Vuex.Store({
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,16 @@ export default {
|
||||
devtool: 'inline-source-map',
|
||||
mode: 'development',
|
||||
entry: fs.readdirSync(__dirname)
|
||||
.filter(entry => entry !== 'ssr')
|
||||
.reduce((entries, dir) => {
|
||||
const fullDir = path.join(__dirname, dir)
|
||||
const entry = path.join(fullDir, 'app.js')
|
||||
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
|
||||
entries[dir] = entry
|
||||
|
||||
if (dir === 'ssr') {
|
||||
entries[dir] = path.join(fullDir, 'browser.js')
|
||||
} else {
|
||||
const entry = path.join(fullDir, 'app.js')
|
||||
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
|
||||
entries[dir] = entry
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}, {}),
|
||||
@@ -27,7 +31,22 @@ export default {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' },
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
useBuiltIns: 'usage',
|
||||
corejs: '2',
|
||||
targets: { ie: 9, safari: '5.1' }
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ test: /\.vue$/, use: 'vue-loader' }
|
||||
]
|
||||
},
|
||||
|
||||
+3
-1
@@ -82,6 +82,7 @@
|
||||
"browserstack-local": "^1.4.0",
|
||||
"chromedriver": "^75.1.0",
|
||||
"codecov": "^3.5.0",
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"eslint": "^6.0.1",
|
||||
"eslint-config-standard": "^13.0.1",
|
||||
"eslint-plugin-import": "^2.18.0",
|
||||
@@ -93,6 +94,7 @@
|
||||
"esm": "^3.2.25",
|
||||
"fs-extra": "^8.1.0",
|
||||
"geckodriver": "^1.16.2",
|
||||
"get-port": "^5.0.0",
|
||||
"is-wsl": "^2.1.0",
|
||||
"jest": "^24.8.0",
|
||||
"jest-environment-jsdom": "^24.8.0",
|
||||
@@ -103,7 +105,7 @@
|
||||
"puppeteer-core": "^1.18.1",
|
||||
"rimraf": "^2.6.3",
|
||||
"rollup": "^1.17.0",
|
||||
"rollup-plugin-buble": "^0.19.8",
|
||||
"rollup-plugin-babel": "^4.3.3",
|
||||
"rollup-plugin-commonjs": "^10.0.1",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import json from 'rollup-plugin-json'
|
||||
import buble from 'rollup-plugin-buble'
|
||||
import babel from 'rollup-plugin-babel'
|
||||
import replace from 'rollup-plugin-replace'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import defaultsDeep from 'lodash/defaultsDeep'
|
||||
@@ -32,8 +32,8 @@ function rollupConfig({
|
||||
}
|
||||
}
|
||||
|
||||
// keep simple polyfills when buble plugin is used for build
|
||||
if (plugins && plugins.some(p => p.name === 'buble')) {
|
||||
// keep simple polyfills when babel plugin is used for build
|
||||
if (plugins && plugins.some(p => p.name === 'babel')) {
|
||||
replaceConfig.values = {
|
||||
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = true',
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export default [
|
||||
file: pkg.web,
|
||||
},
|
||||
plugins: [
|
||||
buble()
|
||||
babel()
|
||||
]
|
||||
},
|
||||
// minimized umd web build
|
||||
@@ -72,7 +72,7 @@ export default [
|
||||
file: pkg.web.replace('.js', '.min.js'),
|
||||
},
|
||||
plugins: [
|
||||
buble(),
|
||||
babel(),
|
||||
terser()
|
||||
]
|
||||
},
|
||||
@@ -84,7 +84,7 @@ export default [
|
||||
format: 'cjs'
|
||||
},
|
||||
plugins: [
|
||||
buble()
|
||||
babel()
|
||||
],
|
||||
external: Object.keys(pkg.dependencies)
|
||||
},
|
||||
@@ -96,7 +96,7 @@ export default [
|
||||
format: 'es'
|
||||
},
|
||||
plugins: [
|
||||
buble()
|
||||
babel()
|
||||
],
|
||||
external: Object.keys(pkg.dependencies)
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { showWarningNotSupported } from '../shared/constants'
|
||||
import { showWarningNotSupported } from '../shared/log'
|
||||
import { getOptions } from '../shared/options'
|
||||
import { pause, resume } from '../shared/pausing'
|
||||
import refresh from './refresh'
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { toArray } from '../utils/array'
|
||||
|
||||
const callbacks = []
|
||||
|
||||
export function isDOMLoaded (d = document) {
|
||||
return d.readyState !== 'loading'
|
||||
}
|
||||
|
||||
export function isDOMComplete (d = document) {
|
||||
return d.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
|
||||
|
||||
for (const tag of tags) {
|
||||
if (!tag[tagIDKeyName] || !tag.callback) {
|
||||
continue
|
||||
}
|
||||
|
||||
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) {
|
||||
for (const [query, callback] of callbacks) {
|
||||
const selector = `${query}[onload="this.__vm_l=1"]`
|
||||
|
||||
let elements = []
|
||||
if (!matchElement) {
|
||||
elements = toArray(document.querySelectorAll(selector))
|
||||
}
|
||||
|
||||
if (matchElement && matchElement.matches(selector)) {
|
||||
elements = [matchElement]
|
||||
}
|
||||
|
||||
for (const element of elements) {
|
||||
/* __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) {
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
element.removeAttribute('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()
|
||||
continue
|
||||
}
|
||||
|
||||
if (!element.__vm_ev) {
|
||||
element.__vm_ev = true
|
||||
|
||||
element.addEventListener('load', onload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { metaInfoOptionKeys, metaInfoAttributeKeys } from '../shared/constants'
|
||||
import { metaInfoOptionKeys, metaInfoAttributeKeys, tagsSupportingOnload } from '../shared/constants'
|
||||
import { isArray } from '../utils/is-type'
|
||||
import { includes } from '../utils/array'
|
||||
import { getTag } from '../utils/elements'
|
||||
import { addCallbacks, addListeners } from './load'
|
||||
import { updateAttribute, updateTag, updateTitle } from './updaters'
|
||||
|
||||
/**
|
||||
@@ -21,6 +22,19 @@ export default function updateClientMetaInfo (appId, options = {}, newInfo) {
|
||||
if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) {
|
||||
// remove the server render attribute so we can update on (next) changes
|
||||
htmlTag.removeAttribute(ssrAttribute)
|
||||
|
||||
// add load callbacks if the
|
||||
let addLoadListeners = false
|
||||
for (const type of tagsSupportingOnload) {
|
||||
if (newInfo[type] && addCallbacks(options, type, newInfo[type])) {
|
||||
addLoadListeners = true
|
||||
}
|
||||
}
|
||||
|
||||
if (addLoadListeners) {
|
||||
addListeners()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+42
-28
@@ -10,7 +10,9 @@ import { queryElements, getElementsKey } from '../../utils/elements.js'
|
||||
* @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, { attribute, tagIDKeyName } = {}, type, tags, head, body) {
|
||||
export default function updateTag (appId, options = {}, type, tags, head, body) {
|
||||
const { attribute, tagIDKeyName } = options
|
||||
|
||||
const dataAttributes = [tagIDKeyName, ...commonDataAttributes]
|
||||
const newElements = []
|
||||
|
||||
@@ -36,38 +38,50 @@ export default function updateTag (appId, { attribute, tagIDKeyName } = {}, type
|
||||
|
||||
if (tags.length) {
|
||||
for (const tag of tags) {
|
||||
if (tag.skip) {
|
||||
continue
|
||||
}
|
||||
|
||||
const newElement = document.createElement(type)
|
||||
newElement.setAttribute(attribute, appId)
|
||||
|
||||
for (const attr in tag) {
|
||||
if (tag.hasOwnProperty(attr)) {
|
||||
if (attr === 'innerHTML') {
|
||||
newElement.innerHTML = tag.innerHTML
|
||||
continue
|
||||
}
|
||||
|
||||
if (attr === 'cssText') {
|
||||
if (newElement.styleSheet) {
|
||||
/* istanbul ignore next */
|
||||
newElement.styleSheet.cssText = tag.cssText
|
||||
} else {
|
||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const _attr = includes(dataAttributes, attr)
|
||||
? `data-${attr}`
|
||||
: attr
|
||||
|
||||
const isBooleanAttribute = includes(booleanHtmlAttributes, attr)
|
||||
if (isBooleanAttribute && !tag[attr]) {
|
||||
continue
|
||||
}
|
||||
|
||||
const value = isBooleanAttribute ? '' : tag[attr]
|
||||
newElement.setAttribute(_attr, value)
|
||||
/* istanbul ignore next */
|
||||
if (!tag.hasOwnProperty(attr)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (attr === 'innerHTML') {
|
||||
newElement.innerHTML = tag.innerHTML
|
||||
continue
|
||||
}
|
||||
|
||||
if (attr === 'cssText') {
|
||||
if (newElement.styleSheet) {
|
||||
/* istanbul ignore next */
|
||||
newElement.styleSheet.cssText = tag.cssText
|
||||
} else {
|
||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (attr === 'callback') {
|
||||
newElement.onload = () => tag[attr](newElement)
|
||||
continue
|
||||
}
|
||||
|
||||
const _attr = includes(dataAttributes, attr)
|
||||
? `data-${attr}`
|
||||
: attr
|
||||
|
||||
const isBooleanAttribute = includes(booleanHtmlAttributes, attr)
|
||||
if (isBooleanAttribute && !tag[attr]) {
|
||||
continue
|
||||
}
|
||||
|
||||
const value = isBooleanAttribute ? '' : tag[attr]
|
||||
newElement.setAttribute(_attr, value)
|
||||
}
|
||||
|
||||
const oldElements = currentElements[getElementsKey(tag)]
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { booleanHtmlAttributes, tagsWithoutEndTag, tagsWithInnerContent, tagAttributeAsInnerContent, commonDataAttributes } from '../../shared/constants'
|
||||
import {
|
||||
booleanHtmlAttributes,
|
||||
tagsWithoutEndTag,
|
||||
tagsWithInnerContent,
|
||||
tagAttributeAsInnerContent,
|
||||
commonDataAttributes
|
||||
} from '../../shared/constants'
|
||||
|
||||
/**
|
||||
* Generates meta, base, link, style, script, noscript tags for use on the server
|
||||
@@ -8,12 +14,16 @@ import { booleanHtmlAttributes, tagsWithoutEndTag, tagsWithInnerContent, tagAttr
|
||||
* @return {Object} - the tag generator
|
||||
*/
|
||||
export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}, type, tags) {
|
||||
const dataAttributes = [tagIDKeyName, ...commonDataAttributes]
|
||||
const dataAttributes = [tagIDKeyName, 'callback', ...commonDataAttributes]
|
||||
|
||||
return {
|
||||
text ({ body = false, pbody = false } = {}) {
|
||||
// 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) {
|
||||
@@ -24,11 +34,13 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
|
||||
return tagsStr
|
||||
}
|
||||
|
||||
let attrs = tag.once ? '' : ` ${attribute}="${ssrAppId}"`
|
||||
|
||||
// build a string containing all attributes of this tag
|
||||
const attrs = tagKeys.reduce((attrsStr, attr) => {
|
||||
for (const attr in tag) {
|
||||
// these attributes are treated as children on the tag
|
||||
if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') {
|
||||
return attrsStr
|
||||
continue
|
||||
}
|
||||
|
||||
// these form the attribute list for this tag
|
||||
@@ -37,23 +49,23 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
|
||||
prefix = 'data-'
|
||||
}
|
||||
|
||||
const isBooleanAttr = booleanHtmlAttributes.includes(attr)
|
||||
if (isBooleanAttr && !tag[attr]) {
|
||||
return attrsStr
|
||||
if (attr === 'callback') {
|
||||
attrs += ` onload="this.__vm_l=1"`
|
||||
continue
|
||||
}
|
||||
|
||||
return isBooleanAttr
|
||||
? `${attrsStr} ${prefix}${attr}`
|
||||
: `${attrsStr} ${prefix}${attr}="${tag[attr]}"`
|
||||
}, '')
|
||||
const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr)
|
||||
if (isBooleanAttr && !tag[attr]) {
|
||||
continue
|
||||
}
|
||||
|
||||
attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
|
||||
}
|
||||
|
||||
// grab child content from one of these attributes, if possible
|
||||
const content = tag.innerHTML || tag.cssText || ''
|
||||
|
||||
// generate tag exactly without any other redundant attribute
|
||||
const observeTag = tag.once
|
||||
? ''
|
||||
: `${attribute}="${ssrAppId}"`
|
||||
|
||||
// these tags have no end tag
|
||||
const hasEndTag = !tagsWithoutEndTag.includes(type)
|
||||
@@ -62,9 +74,8 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
|
||||
const hasContent = hasEndTag && tagsWithInnerContent.includes(type)
|
||||
|
||||
// the final string for this specific tag
|
||||
return !hasContent
|
||||
? `${tagsStr}<${type} ${observeTag}${attrs}${hasEndTag ? '/' : ''}>`
|
||||
: `${tagsStr}<${type} ${observeTag}${attrs}>${content}</${type}>`
|
||||
return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` +
|
||||
(hasContent ? `${content}</${type}>` : '')
|
||||
}, '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ export const metaInfoAttributeKeys = [
|
||||
'bodyAttrs'
|
||||
]
|
||||
|
||||
// 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']
|
||||
@@ -137,6 +140,3 @@ export const booleanHtmlAttributes = [
|
||||
'typemustmatch',
|
||||
'visible'
|
||||
]
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
export const showWarningNotSupported = () => console.warn('This vue app/component has no vue-meta configuration')
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { hasGlobalWindow } from '../utils/window'
|
||||
|
||||
const _global = hasGlobalWindow ? window : global
|
||||
|
||||
const console = (_global.console = _global.console || {})
|
||||
|
||||
export function warn (...args) {
|
||||
/* istanbul ignore next */
|
||||
if (!console || !console.warn) {
|
||||
return
|
||||
}
|
||||
|
||||
console.warn(...args)
|
||||
}
|
||||
|
||||
export const showWarningNotSupported = () => warn('This vue app/component has no vue-meta configuration')
|
||||
+4
-4
@@ -1,7 +1,8 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import { findIndex } from '../utils/array'
|
||||
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,
|
||||
@@ -80,9 +81,8 @@ export function merge (target, source, options = {}) {
|
||||
|
||||
for (const key in source[attrKey]) {
|
||||
if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) {
|
||||
if (booleanHtmlAttributes.includes(key)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details')
|
||||
if (includes(booleanHtmlAttributes, key)) {
|
||||
warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details')
|
||||
}
|
||||
delete source[attrKey][key]
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ import { isUndefined, isFunction } from '../utils/is-type'
|
||||
import { ensuredPush } from '../utils/ensure'
|
||||
import { hasMetaInfo } from './meta-helpers'
|
||||
import { addNavGuards } from './nav-guards'
|
||||
import { warn } from './log'
|
||||
|
||||
let appId = 1
|
||||
|
||||
@@ -18,7 +19,7 @@ export default function createMixin (Vue, options) {
|
||||
get () {
|
||||
// Show deprecation warning once when devtools enabled
|
||||
if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) {
|
||||
console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead') // eslint-disable-line no-console
|
||||
warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead')
|
||||
this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true
|
||||
}
|
||||
return hasMetaInfo(this)
|
||||
|
||||
@@ -5,6 +5,7 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import env from 'node-env-file'
|
||||
import { createBrowser } from 'tib'
|
||||
import { getPort } from '../utils/build'
|
||||
|
||||
const browserString = process.env.BROWSER_STRING || 'puppeteer/core'
|
||||
|
||||
@@ -21,13 +22,17 @@ describe(browserString, () => {
|
||||
}
|
||||
}
|
||||
|
||||
const port = await getPort()
|
||||
|
||||
browser = await createBrowser(browserString, {
|
||||
folder,
|
||||
staticServer: {
|
||||
folder,
|
||||
port
|
||||
},
|
||||
extendPage (page) {
|
||||
return {
|
||||
async navigate (path) {
|
||||
// IMPORTANT: use (arrow) function with block'ed body
|
||||
// see: https://github.com/tunnckoCoreLabs/parse-function/issues/179
|
||||
await page.runAsyncScript((path) => {
|
||||
return new Promise((resolve) => {
|
||||
const oldTitle = document.title
|
||||
@@ -58,6 +63,8 @@ describe(browserString, () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// browser.setLogLevel(['warn', 'error', 'log', 'info'])
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -94,6 +101,14 @@ 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.loadCallback
|
||||
})).toBe('yes')
|
||||
})
|
||||
|
||||
test('/about', async () => {
|
||||
|
||||
Vendored
+2
-1
@@ -5,7 +5,7 @@
|
||||
{{ title.text() }}
|
||||
{{ link.text() }}
|
||||
{{ style.text() }}
|
||||
{{ webpackAssets }}
|
||||
{{ headAssets }}
|
||||
{{ script.text() }}
|
||||
{{ noscript.text() }}
|
||||
</head>
|
||||
@@ -13,6 +13,7 @@
|
||||
{{ script.text({ pbody: true }) }}
|
||||
{{ noscript.text({ pbody: true }) }}
|
||||
{{ app }}
|
||||
{{ bodyAssets }}
|
||||
{{ script.text({ body: true }) }}
|
||||
{{ noscript.text({ body: true }) }}
|
||||
</body>
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
window.loadTest = 'loaded'
|
||||
Vendored
+2
-1
@@ -24,7 +24,8 @@ export default {
|
||||
],
|
||||
script: [
|
||||
{ vmid: 'ldjson', innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }', type: 'application/ld+json' },
|
||||
{ innerHTML: '{ "more": "data" }', type: 'application/ld+json' }
|
||||
{ innerHTML: '{ "more": "data" }', type: 'application/ld+json' },
|
||||
{ vmid: 'loadtest', src: '/load-test.js', body: true, async: true, callback: () => (window.loadCallback = 'yes') }
|
||||
],
|
||||
noscript: [
|
||||
{ innerHTML: '{ "pbody": "yes" }', pbody: true, type: 'application/ld+json' },
|
||||
|
||||
@@ -3,8 +3,6 @@ import { defaultOptions } from '../../src/shared/constants'
|
||||
import metaInfoData from '../utils/meta-info-data'
|
||||
import { titleGenerator } from '../../src/server/generators'
|
||||
|
||||
defaultOptions.ssrAppId = 'test'
|
||||
|
||||
const generateServerInjector = (type, data) => _generateServerInjector(defaultOptions, type, data)
|
||||
|
||||
describe('generators', () => {
|
||||
@@ -88,7 +86,7 @@ describe('extra tests', () => {
|
||||
|
||||
expect(scriptTags.text()).toBe('')
|
||||
expect(scriptTags.text({ body: true })).toBe('')
|
||||
expect(scriptTags.text({ pbody: true })).toBe('<script data-vue-meta="test" 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', () => {
|
||||
@@ -96,7 +94,7 @@ describe('extra tests', () => {
|
||||
const scriptTags = generateServerInjector('script', tags)
|
||||
|
||||
expect(scriptTags.text()).toBe('')
|
||||
expect(scriptTags.text({ body: true })).toBe('<script data-vue-meta="test" 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('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { pTick, createDOM } from '../utils'
|
||||
|
||||
const onLoadAttribute = {
|
||||
k: 'onload',
|
||||
v: 'this.__vm_l=1'
|
||||
}
|
||||
|
||||
const getLoadAttribute = () => `${onLoadAttribute.k}="${onLoadAttribute.v}"`
|
||||
|
||||
describe('load callbacks', () => {
|
||||
let load
|
||||
beforeEach(async () => {
|
||||
jest.resetModules()
|
||||
load = await import('../../src/client/load')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
test('isDOMLoaded', async () => {
|
||||
jest.useRealTimers()
|
||||
const { document } = createDOM()
|
||||
await pTick()
|
||||
|
||||
jest.spyOn(document, 'readyState', 'get').mockReturnValue('loading')
|
||||
expect(load.isDOMLoaded(document)).toBe(false)
|
||||
|
||||
jest.spyOn(document, 'readyState', 'get').mockReturnValue('interactive')
|
||||
expect(load.isDOMLoaded(document)).toBe(true)
|
||||
|
||||
jest.spyOn(document, 'readyState', 'get').mockReturnValue('complete')
|
||||
expect(load.isDOMLoaded(document)).toBe(true)
|
||||
})
|
||||
|
||||
test('isDOMComplete', async () => {
|
||||
jest.useRealTimers()
|
||||
const { document } = createDOM()
|
||||
await pTick()
|
||||
|
||||
jest.spyOn(document, 'readyState', 'get').mockReturnValue('loading')
|
||||
expect(load.isDOMComplete(document)).toBe(false)
|
||||
|
||||
jest.spyOn(document, 'readyState', 'get').mockReturnValue('interactive')
|
||||
expect(load.isDOMComplete(document)).toBe(false)
|
||||
|
||||
jest.spyOn(document, 'readyState', 'get').mockReturnValue('complete')
|
||||
expect(load.isDOMComplete(document)).toBe(true)
|
||||
})
|
||||
|
||||
test('waitDOMLoaded', async () => {
|
||||
expect(load.waitDOMLoaded()).toBe(true)
|
||||
|
||||
jest.spyOn(document, 'readyState', 'get').mockReturnValue('loading')
|
||||
const waitPromise = load.waitDOMLoaded()
|
||||
expect(waitPromise).toEqual(expect.any(Promise))
|
||||
|
||||
const domLoaded = new Event('DOMContentLoaded')
|
||||
document.dispatchEvent(domLoaded)
|
||||
|
||||
await expect(waitPromise).resolves.toEqual(expect.any(Object))
|
||||
})
|
||||
|
||||
test('addCallback (no query)', () => {
|
||||
const callback = () => {}
|
||||
load.addCallback(callback)
|
||||
|
||||
const matches = jest.fn(() => false)
|
||||
load.applyCallbacks({ matches })
|
||||
|
||||
expect(matches).toHaveBeenCalledTimes(1)
|
||||
expect(matches).toHaveBeenCalledWith(`[${getLoadAttribute()}]`)
|
||||
})
|
||||
|
||||
test('addCallback (query)', () => {
|
||||
const callback = () => {}
|
||||
load.addCallback('script', callback)
|
||||
|
||||
const matches = jest.fn(() => false)
|
||||
load.applyCallbacks({ matches })
|
||||
|
||||
expect(matches).toHaveBeenCalledTimes(1)
|
||||
expect(matches).toHaveBeenCalledWith(`script[${getLoadAttribute()}]`)
|
||||
})
|
||||
|
||||
test('addCallbacks', () => {
|
||||
const addListeners = jest.spyOn(document, 'querySelectorAll').mockReturnValue(false)
|
||||
|
||||
const config = { tagIDKeyName: 'test-id' }
|
||||
|
||||
const tags = [
|
||||
{ [config.tagIDKeyName]: 'test1', callback: false },
|
||||
{ [config.tagIDKeyName]: false, callback: () => {} },
|
||||
{ [config.tagIDKeyName]: 'test1', callback: () => {} },
|
||||
{ [config.tagIDKeyName]: 'test2', callback: () => {} }
|
||||
]
|
||||
|
||||
load.addCallbacks(config, 'link', tags)
|
||||
|
||||
const matches = jest.fn(() => false)
|
||||
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(addListeners).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('addCallbacks (auto add listeners)', () => {
|
||||
const addListeners = jest.spyOn(document, 'querySelectorAll').mockReturnValue(false)
|
||||
|
||||
const config = { tagIDKeyName: 'test-id', loadCallbackAttribute: 'test-load' }
|
||||
|
||||
const tags = [
|
||||
{ [config.tagIDKeyName]: 'test1', callback: () => {} }
|
||||
]
|
||||
|
||||
load.addCallbacks(config, 'style', tags, true)
|
||||
|
||||
const matches = jest.fn(() => false)
|
||||
load.applyCallbacks({ matches })
|
||||
|
||||
expect(matches).toHaveBeenCalledTimes(1)
|
||||
expect(matches).toHaveBeenCalledWith(`style[data-${config.tagIDKeyName}="test1"][${getLoadAttribute()}]`)
|
||||
|
||||
expect(addListeners).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('callback trigger', () => {
|
||||
const { window, document } = createDOM()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
const el = document.createElement('script')
|
||||
el.setAttribute(onLoadAttribute.k, onLoadAttribute.v)
|
||||
document.body.appendChild(el)
|
||||
|
||||
load.addCallback(callback)
|
||||
load.applyCallbacks(el)
|
||||
|
||||
const loadEvent = new window.Event('load')
|
||||
el.dispatchEvent(loadEvent)
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('callback trigger (loaded before adding)', () => {
|
||||
const { document } = createDOM()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
const el = document.createElement('script')
|
||||
el.setAttribute(onLoadAttribute.k, onLoadAttribute.v)
|
||||
el.__vm_l = 1
|
||||
document.body.appendChild(el)
|
||||
|
||||
load.addCallback(callback)
|
||||
load.applyCallbacks(el)
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('callback trigger (only once)', () => {
|
||||
const { window, document } = createDOM()
|
||||
|
||||
const callback = jest.fn()
|
||||
|
||||
const el = document.createElement('script')
|
||||
el.setAttribute(onLoadAttribute.k, onLoadAttribute.v)
|
||||
document.body.appendChild(el)
|
||||
|
||||
el.__vm_l = 1
|
||||
|
||||
load.addCallback(callback)
|
||||
load.applyCallbacks(el)
|
||||
|
||||
el.__vm_cb = true
|
||||
|
||||
const loadEvent = new window.Event('load')
|
||||
el.dispatchEvent(loadEvent)
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('only one event listener added', () => {
|
||||
const { window, document } = createDOM()
|
||||
|
||||
const el = document.createElement('script')
|
||||
const addEventListener = el.addEventListener.bind(el)
|
||||
const addEventListenerSpy = jest.spyOn(el, 'addEventListener').mockImplementation((...args) => {
|
||||
return addEventListener(...args)
|
||||
})
|
||||
el.setAttribute(onLoadAttribute.k, onLoadAttribute.v)
|
||||
document.body.appendChild(el)
|
||||
|
||||
load.addCallback(() => {})
|
||||
load.applyCallbacks(el)
|
||||
|
||||
const loadEvent1 = new window.Event('load')
|
||||
el.dispatchEvent(loadEvent1)
|
||||
|
||||
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
el.setAttribute(onLoadAttribute.k, onLoadAttribute.v)
|
||||
load.applyCallbacks(el)
|
||||
|
||||
const loadEvent2 = new window.Event('load')
|
||||
el.dispatchEvent(loadEvent2)
|
||||
|
||||
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,9 @@
|
||||
import _updateClientMetaInfo from '../../src/client/updateClientMetaInfo'
|
||||
import { defaultOptions } 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'
|
||||
|
||||
const updateClientMetaInfo = (type, data) => _updateClientMetaInfo('test', defaultOptions, { [type]: data })
|
||||
const updateClientMetaInfo = (type, data) => _updateClientMetaInfo(ssrAppId, defaultOptions, { [type]: data })
|
||||
|
||||
describe('updaters', () => {
|
||||
let html
|
||||
@@ -14,7 +15,7 @@ describe('updaters', () => {
|
||||
Array.from(html.getElementsByTagName('meta')).forEach(el => el.parentNode.removeChild(el))
|
||||
})
|
||||
|
||||
Object.keys(metaInfoData).forEach((type) => {
|
||||
for (const type in metaInfoData) {
|
||||
const typeTests = metaInfoData[type]
|
||||
|
||||
const testCases = {
|
||||
@@ -93,5 +94,22 @@ describe('updaters', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('extra tests', () => {
|
||||
test('adds callback listener on hydration', () => {
|
||||
const addListeners = load.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 tags = updateClientMetaInfo('script', data)
|
||||
|
||||
expect(tags).toBe(false)
|
||||
expect(html.hasAttribute(ssrAttribute)).toBe(false)
|
||||
expect(addListenersSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import puppeteer from 'puppeteer-core'
|
||||
|
||||
import ChromeDetector from './chrome'
|
||||
|
||||
export default class Browser {
|
||||
constructor () {
|
||||
this.detector = new ChromeDetector()
|
||||
}
|
||||
|
||||
async start (options = {}) {
|
||||
// https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions
|
||||
const _opts = {
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox'
|
||||
],
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
...options
|
||||
}
|
||||
|
||||
if (!_opts.executablePath) {
|
||||
_opts.executablePath = this.detector.detect()
|
||||
}
|
||||
|
||||
this.browser = await puppeteer.launch(_opts)
|
||||
}
|
||||
|
||||
async close () {
|
||||
if (!this.browser) { return }
|
||||
await this.browser.close()
|
||||
}
|
||||
|
||||
async page (url, globalName = 'vueMeta') {
|
||||
if (!this.browser) { throw new Error('Please call start() before page(url)') }
|
||||
const page = await this.browser.newPage()
|
||||
|
||||
// pass on console messages
|
||||
const typeMap = {
|
||||
debug: 'debug',
|
||||
warning: 'warn',
|
||||
error: 'error'
|
||||
}
|
||||
page.on('console', (msg) => {
|
||||
if (typeMap[msg.type()]) {
|
||||
console[typeMap[msg.type()]](msg.text()) // eslint-disable-line no-console
|
||||
}
|
||||
})
|
||||
|
||||
await page.goto(url)
|
||||
page.$globalHandle = `window.$${globalName}`
|
||||
await page.waitForFunction(`!!${page.$globalHandle}`)
|
||||
page.html = () => page.evaluate(() => window.document.documentElement.outerHTML)
|
||||
page.$text = (selector, trim) => page.$eval(selector, (el, trim) => {
|
||||
return trim ? el.textContent.replace(/^\s+|\s+$/g, '') : el.textContent
|
||||
}, trim)
|
||||
page.$$text = (selector, trim) =>
|
||||
page.$$eval(selector, (els, trim) => els.map((el) => {
|
||||
return trim ? el.textContent.replace(/^\s+|\s+$/g, '') : el.textContent
|
||||
}), trim)
|
||||
page.$attr = (selector, attr) =>
|
||||
page.$eval(selector, (el, attr) => el.getAttribute(attr), attr)
|
||||
page.$$attr = (selector, attr) =>
|
||||
page.$$eval(
|
||||
selector,
|
||||
(els, attr) => els.map(el => el.getAttribute(attr)),
|
||||
attr
|
||||
)
|
||||
|
||||
page.$vueMeta = await page.evaluateHandle(page.$globalHandle)
|
||||
|
||||
page.vueMeta = {
|
||||
async navigate (path, waitEnd = true) {
|
||||
const hook = page.evaluate(`
|
||||
new Promise(resolve =>
|
||||
${page.$globalHandle}.$once('routeChanged', resolve)
|
||||
).then(() => new Promise(resolve => setTimeout(resolve, 50)))
|
||||
`)
|
||||
await page.evaluate(
|
||||
($vueMeta, path) => $vueMeta.$router.push(path),
|
||||
page.$vueMeta,
|
||||
path
|
||||
)
|
||||
if (waitEnd) {
|
||||
await hook
|
||||
}
|
||||
return { hook }
|
||||
},
|
||||
routeData () {
|
||||
return page.evaluate(($vueMeta) => {
|
||||
return {
|
||||
path: $vueMeta.$route.path,
|
||||
query: $vueMeta.$route.query
|
||||
}
|
||||
}, page.$vueMeta)
|
||||
}
|
||||
}
|
||||
return page
|
||||
}
|
||||
}
|
||||
+18
-3
@@ -2,11 +2,14 @@ import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import { template } from 'lodash'
|
||||
import webpack from 'webpack'
|
||||
import CopyWebpackPlugin from 'copy-webpack-plugin'
|
||||
import VueLoaderPlugin from 'vue-loader/lib/plugin'
|
||||
import { createRenderer } from 'vue-server-renderer'
|
||||
|
||||
const renderer = createRenderer()
|
||||
|
||||
export { default as getPort } from 'get-port'
|
||||
|
||||
export function webpackRun (config) {
|
||||
const compiler = webpack(config)
|
||||
|
||||
@@ -50,13 +53,22 @@ export async function buildFixture (fixture, config = {}) {
|
||||
const templateFile = await fs.readFile(path.resolve(fixturePath, '..', 'app.template.html'), { encoding: 'utf8' })
|
||||
const compiled = template(templateFile, { interpolate: /{{([\s\S]+?)}}/g })
|
||||
|
||||
const webpackAssets = webpackStats.assets.reduce((s, asset) => `${s}<script src="./${asset.name}"${asset.name.includes('chunk') ? '' : ' defer'}></script>\n`, '')
|
||||
const assets = webpackStats.assets.filter(asset => !asset.name.includes('load-test'))
|
||||
|
||||
const headAssets = assets
|
||||
.filter(asset => asset.name.includes('chunk'))
|
||||
.reduce((s, asset) => `${s}<script src="./${asset.name}"></script>\n`, '')
|
||||
|
||||
const bodyAssets = assets
|
||||
.filter(asset => !asset.name.includes('chunk'))
|
||||
.reduce((s, asset) => `${s}<script src="./${asset.name}"></script>\n`, '')
|
||||
|
||||
const app = await renderer.renderToString(vueApp)
|
||||
// !!! run inject after renderToString !!!
|
||||
const metaInfo = vueApp.$meta().inject()
|
||||
|
||||
const appFile = path.resolve(webpackStats.outputPath, 'index.html')
|
||||
const html = compiled({ app, webpackAssets, ...metaInfo })
|
||||
const html = compiled({ app, headAssets, bodyAssets, ...metaInfo })
|
||||
|
||||
await fs.writeFile(appFile, html)
|
||||
|
||||
@@ -125,7 +137,10 @@ export function createWebpackConfig (config = {}) {
|
||||
// make sure our simple polyfills are enabled
|
||||
'NODE_ENV': '"test"'
|
||||
}
|
||||
})
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
{ from: path.join(path.dirname(config.entry), 'static') }
|
||||
])
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
/**
|
||||
* @license Copyright 2016 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { execSync, execFileSync } from 'child_process'
|
||||
import isWsl from 'is-wsl'
|
||||
import uniq from 'lodash/uniq'
|
||||
|
||||
const newLineRegex = /\r?\n/
|
||||
|
||||
/**
|
||||
* This class is based on node-get-chrome
|
||||
* https://github.com/mrlee23/node-get-chrome
|
||||
* https://github.com/gwuhaolin/chrome-finder
|
||||
*/
|
||||
export default class ChromeDetector {
|
||||
constructor () {
|
||||
this.platform = isWsl ? 'wsl' : process.platform
|
||||
}
|
||||
|
||||
detect (platform = this.platform) {
|
||||
const handler = this[platform]
|
||||
if (typeof handler !== 'function') {
|
||||
throw new TypeError(`${platform} is not supported.`)
|
||||
}
|
||||
return this[platform]()[0]
|
||||
}
|
||||
|
||||
darwin () {
|
||||
const suffixes = [
|
||||
'/Contents/MacOS/Chromium',
|
||||
'/Contents/MacOS/Google Chrome Canary',
|
||||
'/Contents/MacOS/Google Chrome'
|
||||
]
|
||||
const LSREGISTER =
|
||||
'/System/Library/Frameworks/CoreServices.framework' +
|
||||
'/Versions/A/Frameworks/LaunchServices.framework' +
|
||||
'/Versions/A/Support/lsregister'
|
||||
const installations = []
|
||||
const customChromePath = this.resolveChromePath()
|
||||
if (customChromePath) {
|
||||
installations.push(customChromePath)
|
||||
}
|
||||
execSync(
|
||||
`${LSREGISTER} -dump` +
|
||||
" | grep -i '(google chrome\\( canary\\)\\?|chromium).app$'" +
|
||||
' | awk \'{$1=""; print $0}\''
|
||||
)
|
||||
.toString()
|
||||
.split(newLineRegex)
|
||||
.forEach((inst) => {
|
||||
suffixes.forEach((suffix) => {
|
||||
const execPath = path.join(inst.trim(), suffix)
|
||||
if (this.canAccess(execPath)) {
|
||||
installations.push(execPath)
|
||||
}
|
||||
})
|
||||
})
|
||||
// Retains one per line to maintain readability.
|
||||
// clang-format off
|
||||
const priorities = [
|
||||
{ regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome.app`), weight: 50 },
|
||||
{ regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome Canary.app`), weight: 51 },
|
||||
{ regex: new RegExp(`^${process.env.HOME}/Applications/.*Chromium.app`), weight: 52 },
|
||||
{ regex: /^\/Applications\/.*Chrome.app/, weight: 100 },
|
||||
{ regex: /^\/Applications\/.*Chrome Canary.app/, weight: 101 },
|
||||
{ regex: /^\/Applications\/.*Chromium.app/, weight: 102 },
|
||||
{ regex: /^\/Volumes\/.*Chrome.app/, weight: -3 },
|
||||
{ regex: /^\/Volumes\/.*Chrome Canary.app/, weight: -2 },
|
||||
{ regex: /^\/Volumes\/.*Chromium.app/, weight: -1 }
|
||||
]
|
||||
if (process.env.LIGHTHOUSE_CHROMIUM_PATH) {
|
||||
priorities.push({ regex: new RegExp(process.env.LIGHTHOUSE_CHROMIUM_PATH), weight: 150 })
|
||||
}
|
||||
if (process.env.CHROME_PATH) {
|
||||
priorities.push({ regex: new RegExp(process.env.CHROME_PATH), weight: 151 })
|
||||
}
|
||||
// clang-format on
|
||||
return this.sort(installations, priorities)
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for linux executables in 3 ways
|
||||
* 1. Look into CHROME_PATH env variable
|
||||
* 2. Look into the directories where .desktop are saved on gnome based distro's
|
||||
* 3. Look for google-chrome-stable & google-chrome executables by using the which command
|
||||
*/
|
||||
linux () {
|
||||
let installations = []
|
||||
// 1. Look into CHROME_PATH env variable
|
||||
const customChromePath = this.resolveChromePath()
|
||||
if (customChromePath) {
|
||||
installations.push(customChromePath)
|
||||
}
|
||||
// 2. Look into the directories where .desktop are saved on gnome based distro's
|
||||
const desktopInstallationFolders = [
|
||||
path.join(require('os').homedir(), '.local/share/applications/'),
|
||||
'/usr/share/applications/'
|
||||
]
|
||||
desktopInstallationFolders.forEach((folder) => {
|
||||
installations = installations.concat(this.findChromeExecutables(folder))
|
||||
})
|
||||
// Look for chromium(-browser) & google-chrome(-stable) executables by using the which command
|
||||
const executables = [
|
||||
'chromium-browser',
|
||||
'chromium',
|
||||
'google-chrome-stable',
|
||||
'google-chrome'
|
||||
]
|
||||
executables.forEach((executable) => {
|
||||
try {
|
||||
const chromePath = execFileSync('which', [executable])
|
||||
.toString()
|
||||
.split(newLineRegex)[0]
|
||||
if (this.canAccess(chromePath)) {
|
||||
installations.push(chromePath)
|
||||
}
|
||||
} catch (e) {
|
||||
// Not installed.
|
||||
}
|
||||
})
|
||||
if (!installations.length) {
|
||||
throw new Error(
|
||||
'The environment variable CHROME_PATH must be set to ' +
|
||||
'executable of a build of Chromium version 54.0 or later.'
|
||||
)
|
||||
}
|
||||
const priorities = [
|
||||
{ regex: /chromium-browser$/, weight: 51 },
|
||||
{ regex: /chromium$/, weight: 50 },
|
||||
{ regex: /chrome-wrapper$/, weight: 49 },
|
||||
{ regex: /google-chrome-stable$/, weight: 48 },
|
||||
{ regex: /google-chrome$/, weight: 47 }
|
||||
]
|
||||
if (process.env.LIGHTHOUSE_CHROMIUM_PATH) {
|
||||
priorities.push({
|
||||
regex: new RegExp(process.env.LIGHTHOUSE_CHROMIUM_PATH),
|
||||
weight: 100
|
||||
})
|
||||
}
|
||||
if (process.env.CHROME_PATH) {
|
||||
priorities.push({ regex: new RegExp(process.env.CHROME_PATH), weight: 101 })
|
||||
}
|
||||
return this.sort(uniq(installations.filter(Boolean)), priorities)
|
||||
}
|
||||
|
||||
wsl () {
|
||||
// Manually populate the environment variables assuming it's the default config
|
||||
process.env.LOCALAPPDATA = this.getLocalAppDataPath(process.env.PATH)
|
||||
process.env.PROGRAMFILES = '/mnt/c/Program Files'
|
||||
process.env['PROGRAMFILES(X86)'] = '/mnt/c/Program Files (x86)'
|
||||
return this.win32()
|
||||
}
|
||||
|
||||
win32 () {
|
||||
const installations = []
|
||||
const sep = path.sep
|
||||
const suffixes = [
|
||||
`${sep}Chromium${sep}Application${sep}chrome.exe`,
|
||||
`${sep}Google${sep}Chrome SxS${sep}Application${sep}chrome.exe`,
|
||||
`${sep}Google${sep}Chrome${sep}Application${sep}chrome.exe`,
|
||||
`${sep}chrome-win32${sep}chrome.exe`,
|
||||
`${sep}Google${sep}Chrome Beta${sep}Application${sep}chrome.exe`
|
||||
]
|
||||
const prefixes = [
|
||||
process.env.LOCALAPPDATA,
|
||||
process.env.PROGRAMFILES,
|
||||
process.env['PROGRAMFILES(X86)']
|
||||
].filter(Boolean)
|
||||
const customChromePath = this.resolveChromePath()
|
||||
if (customChromePath) {
|
||||
installations.push(customChromePath)
|
||||
}
|
||||
prefixes.forEach(prefix =>
|
||||
suffixes.forEach((suffix) => {
|
||||
const chromePath = path.join(prefix, suffix)
|
||||
if (this.canAccess(chromePath)) {
|
||||
installations.push(chromePath)
|
||||
}
|
||||
})
|
||||
)
|
||||
return installations
|
||||
}
|
||||
|
||||
resolveChromePath () {
|
||||
if (this.canAccess(process.env.CHROME_PATH)) {
|
||||
return process.env.CHROME_PATH
|
||||
}
|
||||
if (this.canAccess(process.env.LIGHTHOUSE_CHROMIUM_PATH)) {
|
||||
console.warn( // eslint-disable-line no-console
|
||||
'ChromeLauncher',
|
||||
'LIGHTHOUSE_CHROMIUM_PATH is deprecated, use CHROME_PATH env variable instead.'
|
||||
)
|
||||
return process.env.LIGHTHOUSE_CHROMIUM_PATH
|
||||
}
|
||||
}
|
||||
|
||||
getLocalAppDataPath (path) {
|
||||
const userRegExp = /\/mnt\/([a-z])\/Users\/([^/:]+)\/AppData\//
|
||||
const results = userRegExp.exec(path) || []
|
||||
return `/mnt/${results[1]}/Users/${results[2]}/AppData/Local`
|
||||
}
|
||||
|
||||
sort (installations, priorities) {
|
||||
const defaultPriority = 10
|
||||
return installations
|
||||
.map((inst) => {
|
||||
for (const pair of priorities) {
|
||||
if (pair.regex.test(inst)) {
|
||||
return { path: inst, weight: pair.weight }
|
||||
}
|
||||
}
|
||||
return { path: inst, weight: defaultPriority }
|
||||
})
|
||||
.sort((a, b) => b.weight - a.weight)
|
||||
.map(pair => pair.path)
|
||||
}
|
||||
|
||||
canAccess (file) {
|
||||
if (!file) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
fs.accessSync(file)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
findChromeExecutables (folder) {
|
||||
const argumentsRegex = /(^[^ ]+).*/ // Take everything up to the first space
|
||||
const chromeExecRegex = '^Exec=/.*/(google-chrome|chrome|chromium)-.*'
|
||||
const installations = []
|
||||
if (this.canAccess(folder)) {
|
||||
// Output of the grep & print looks like:
|
||||
// /opt/google/chrome/google-chrome --profile-directory
|
||||
// /home/user/Downloads/chrome-linux/chrome-wrapper %U
|
||||
let execPaths
|
||||
// Some systems do not support grep -R so fallback to -r.
|
||||
// See https://github.com/GoogleChrome/chrome-launcher/issues/46 for more context.
|
||||
try {
|
||||
execPaths = execSync(
|
||||
`grep -ER "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`
|
||||
)
|
||||
} catch (e) {
|
||||
execPaths = execSync(
|
||||
`grep -Er "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`
|
||||
)
|
||||
}
|
||||
execPaths = execPaths
|
||||
.toString()
|
||||
.split(newLineRegex)
|
||||
.map(execPath => execPath.replace(argumentsRegex, '$1'))
|
||||
execPaths.forEach(
|
||||
execPath => this.canAccess(execPath) && installations.push(execPath)
|
||||
)
|
||||
}
|
||||
return installations
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { JSDOM } from 'jsdom'
|
||||
import { mount, shallowMount, createWrapper, createLocalVue } from '@vue/test-utils'
|
||||
import { renderToString } from '@vue/server-test-utils'
|
||||
import { defaultOptions } from '../../src/shared/constants'
|
||||
@@ -32,3 +33,15 @@ export const vmTick = (vm) => {
|
||||
vm.$nextTick(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
export const pTick = () => new Promise(resolve => process.nextTick(resolve))
|
||||
|
||||
export function createDOM (html = '<!DOCTYPE html>', options = {}) {
|
||||
const dom = new JSDOM(html, options)
|
||||
|
||||
return {
|
||||
dom,
|
||||
window: dom.window,
|
||||
document: dom.window.document
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ const metaInfoData = {
|
||||
base: {
|
||||
add: {
|
||||
data: [{ href: 'href' }],
|
||||
expect: ['<base data-vue-meta="test" href="href">']
|
||||
expect: ['<base data-vue-meta="ssr" href="href">']
|
||||
},
|
||||
change: {
|
||||
data: [{ href: 'href2' }],
|
||||
expect: ['<base data-vue-meta="test" href="href2">']
|
||||
expect: ['<base data-vue-meta="ssr" href="href2">']
|
||||
},
|
||||
remove: {
|
||||
data: [],
|
||||
@@ -41,8 +41,8 @@ const metaInfoData = {
|
||||
add: {
|
||||
data: [{ charset: 'utf-8' }, { property: 'a', content: 'a' }],
|
||||
expect: [
|
||||
'<meta data-vue-meta="test" charset="utf-8">',
|
||||
'<meta data-vue-meta="test" property="a" content="a">'
|
||||
'<meta data-vue-meta="ssr" charset="utf-8">',
|
||||
'<meta data-vue-meta="ssr" property="a" content="a">'
|
||||
]
|
||||
},
|
||||
change: {
|
||||
@@ -51,8 +51,8 @@ const metaInfoData = {
|
||||
{ property: 'a', content: 'b' }
|
||||
],
|
||||
expect: [
|
||||
'<meta data-vue-meta="test" charset="utf-16">',
|
||||
'<meta data-vue-meta="test" property="a" content="b">'
|
||||
'<meta data-vue-meta="ssr" charset="utf-16">',
|
||||
'<meta data-vue-meta="ssr" property="a" content="b">'
|
||||
]
|
||||
},
|
||||
// make sure elements that already exists are not unnecessarily updated
|
||||
@@ -62,8 +62,8 @@ const metaInfoData = {
|
||||
{ property: 'a', content: 'c' }
|
||||
],
|
||||
expect: [
|
||||
'<meta data-vue-meta="test" charset="utf-16">',
|
||||
'<meta data-vue-meta="test" property="a" content="c">'
|
||||
'<meta data-vue-meta="ssr" charset="utf-16">',
|
||||
'<meta data-vue-meta="ssr" property="a" content="c">'
|
||||
],
|
||||
test (side, defaultTest) {
|
||||
if (side === 'client') {
|
||||
@@ -85,11 +85,11 @@ const metaInfoData = {
|
||||
link: {
|
||||
add: {
|
||||
data: [{ rel: 'stylesheet', href: 'href' }],
|
||||
expect: ['<link data-vue-meta="test" rel="stylesheet" href="href">']
|
||||
expect: ['<link data-vue-meta="ssr" rel="stylesheet" href="href">']
|
||||
},
|
||||
change: {
|
||||
data: [{ rel: 'stylesheet', href: 'href', media: 'screen' }],
|
||||
expect: ['<link data-vue-meta="test" rel="stylesheet" href="href" media="screen">']
|
||||
expect: ['<link data-vue-meta="ssr" rel="stylesheet" href="href" media="screen">']
|
||||
},
|
||||
remove: {
|
||||
data: [],
|
||||
@@ -99,11 +99,11 @@ const metaInfoData = {
|
||||
style: {
|
||||
add: {
|
||||
data: [{ type: 'text/css', cssText: '.foo { color: red; }' }],
|
||||
expect: ['<style data-vue-meta="test" type="text/css">.foo { color: red; }</style>']
|
||||
expect: ['<style data-vue-meta="ssr" type="text/css">.foo { color: red; }</style>']
|
||||
},
|
||||
change: {
|
||||
data: [{ type: 'text/css', cssText: '.foo { color: blue; }' }],
|
||||
expect: ['<style data-vue-meta="test" type="text/css">.foo { color: blue; }</style>']
|
||||
expect: ['<style data-vue-meta="ssr" type="text/css">.foo { color: blue; }</style>']
|
||||
},
|
||||
remove: {
|
||||
data: [],
|
||||
@@ -113,20 +113,22 @@ const metaInfoData = {
|
||||
script: {
|
||||
add: {
|
||||
data: [
|
||||
{ src: 'src', async: false, defer: true, [defaultOptions.tagIDKeyName]: 'content' },
|
||||
{ src: 'src1', async: false, defer: true, [defaultOptions.tagIDKeyName]: 'content', callback: () => {} },
|
||||
{ src: 'src-prepend', async: true, defer: false, pbody: true },
|
||||
{ src: 'src', async: false, defer: true, body: true }
|
||||
{ src: 'src2', async: false, defer: true, body: true },
|
||||
{ src: 'src3', async: false, skip: true }
|
||||
],
|
||||
expect: [
|
||||
'<script data-vue-meta="test" src="src" defer data-vmid="content"></script>',
|
||||
'<script data-vue-meta="test" src="src-prepend" async data-pbody="true"></script>',
|
||||
'<script data-vue-meta="test" src="src" defer data-body="true"></script>'
|
||||
'<script data-vue-meta="ssr" src="src1" defer data-vmid="content" onload="this.__vm_l=1"></script>',
|
||||
'<script data-vue-meta="ssr" src="src-prepend" async data-pbody="true"></script>',
|
||||
'<script data-vue-meta="ssr" src="src2" defer data-body="true"></script>'
|
||||
],
|
||||
test (side, defaultTest) {
|
||||
return () => {
|
||||
if (side === 'client') {
|
||||
for (const index in this.expect) {
|
||||
this.expect[index] = this.expect[index].replace(/(async|defer)/g, '$1=""')
|
||||
this.expect[index] = this.expect[index].replace(/ onload="this.__vm_l=1"/, '')
|
||||
}
|
||||
const tags = defaultTest()
|
||||
|
||||
@@ -150,7 +152,7 @@ const metaInfoData = {
|
||||
// this test only runs for client so we can directly expect wrong boolean attributes
|
||||
change: {
|
||||
data: [{ src: 'src', async: true, defer: true, [defaultOptions.tagIDKeyName]: 'content2' }],
|
||||
expect: ['<script data-vue-meta="test" src="src" async="" defer="" data-vmid="content2"></script>']
|
||||
expect: ['<script data-vue-meta="ssr" src="src" async="" defer="" data-vmid="content2"></script>']
|
||||
},
|
||||
remove: {
|
||||
data: [],
|
||||
@@ -160,11 +162,11 @@ const metaInfoData = {
|
||||
noscript: {
|
||||
add: {
|
||||
data: [{ innerHTML: '<p>noscript</p>' }],
|
||||
expect: ['<noscript data-vue-meta="test"><p>noscript</p></noscript>']
|
||||
expect: ['<noscript data-vue-meta="ssr"><p>noscript</p></noscript>']
|
||||
},
|
||||
change: {
|
||||
data: [{ innerHTML: '<p>noscript, no really</p>' }],
|
||||
expect: ['<noscript data-vue-meta="test"><p>noscript, no really</p></noscript>']
|
||||
expect: ['<noscript data-vue-meta="ssr"><p>noscript, no really</p></noscript>']
|
||||
},
|
||||
remove: {
|
||||
data: [],
|
||||
|
||||
@@ -1778,7 +1778,7 @@ acorn-globals@^4.1.0, acorn-globals@^4.3.2:
|
||||
acorn "^6.0.1"
|
||||
acorn-walk "^6.0.1"
|
||||
|
||||
acorn-jsx@^5.0.0, acorn-jsx@^5.0.1:
|
||||
acorn-jsx@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
|
||||
integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==
|
||||
@@ -2534,20 +2534,6 @@ bser@^2.0.0:
|
||||
dependencies:
|
||||
node-int64 "^0.4.0"
|
||||
|
||||
buble@^0.19.8:
|
||||
version "0.19.8"
|
||||
resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.8.tgz#d642f0081afab66dccd897d7b6360d94030b9d3d"
|
||||
integrity sha512-IoGZzrUTY5fKXVkgGHw3QeXFMUNBFv+9l8a4QJKG1JhG3nCMHTdEX1DCOg8568E2Q9qvAQIiSokv6Jsgx8p2cA==
|
||||
dependencies:
|
||||
acorn "^6.1.1"
|
||||
acorn-dynamic-import "^4.0.0"
|
||||
acorn-jsx "^5.0.1"
|
||||
chalk "^2.4.2"
|
||||
magic-string "^0.25.3"
|
||||
minimist "^1.2.0"
|
||||
os-homedir "^2.0.0"
|
||||
regexpu-core "^4.5.4"
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||
@@ -3323,7 +3309,7 @@ copy-descriptor@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||
|
||||
copy-webpack-plugin@^5.0.2:
|
||||
copy-webpack-plugin@^5.0.2, copy-webpack-plugin@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.3.tgz#2179e3c8fd69f13afe74da338896f1f01a875b5c"
|
||||
integrity sha512-PlZRs9CUMnAVylZq+vg2Juew662jWtwOXOqH4lbQD9ZFhRG9R7tVStOgHt21CBGVq7k5yIJaz8TXDLSjV+Lj8Q==
|
||||
@@ -5044,6 +5030,13 @@ get-pkg-repo@^1.0.0:
|
||||
parse-github-repo-url "^1.3.0"
|
||||
through2 "^2.0.0"
|
||||
|
||||
get-port@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.0.0.tgz#aa22b6b86fd926dd7884de3e23332c9f70c031a6"
|
||||
integrity sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ==
|
||||
dependencies:
|
||||
type-fest "^0.3.0"
|
||||
|
||||
get-stdin@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||
@@ -7033,13 +7026,6 @@ magic-string@^0.25.2:
|
||||
dependencies:
|
||||
sourcemap-codec "^1.4.4"
|
||||
|
||||
magic-string@^0.25.3:
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.3.tgz#34b8d2a2c7fec9d9bdf9929a3fd81d271ef35be9"
|
||||
integrity sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==
|
||||
dependencies:
|
||||
sourcemap-codec "^1.4.4"
|
||||
|
||||
make-dir@^2.0.0, make-dir@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
||||
@@ -7894,11 +7880,6 @@ os-homedir@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
||||
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
|
||||
|
||||
os-homedir@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-2.0.0.tgz#a0c76bb001a8392a503cbd46e7e650b3423a923c"
|
||||
integrity sha512-saRNz0DSC5C/I++gFIaJTXoFJMRwiP5zHar5vV3xQ2TkgEw6hDCcU5F272JjUylpiVgBrZNQHnfjkLabTfb92Q==
|
||||
|
||||
os-locale@^3.0.0, os-locale@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
|
||||
@@ -9313,13 +9294,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rollup-plugin-buble@^0.19.8:
|
||||
version "0.19.8"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-buble/-/rollup-plugin-buble-0.19.8.tgz#f9232e2bb62a7573d04f9705c1bd6f02c2a02c6a"
|
||||
integrity sha512-8J4zPk2DQdk3rxeZvxgzhHh/rm5nJkjwgcsUYisCQg1QbT5yagW+hehYEW7ZNns/NVbDCTv4JQ7h4fC8qKGOKw==
|
||||
rollup-plugin-babel@^4.3.3:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.3.3.tgz#7eb5ac16d9b5831c3fd5d97e8df77ba25c72a2aa"
|
||||
integrity sha512-tKzWOCmIJD/6aKNz0H1GMM+lW1q9KyFubbWzGiOG540zxPPifnEAHTZwjo0g991Y+DyOZcLqBgqOdqazYE5fkw==
|
||||
dependencies:
|
||||
buble "^0.19.8"
|
||||
rollup-pluginutils "^2.3.3"
|
||||
"@babel/helper-module-imports" "^7.0.0"
|
||||
rollup-pluginutils "^2.8.1"
|
||||
|
||||
rollup-plugin-commonjs@^10.0.1:
|
||||
version "10.0.1"
|
||||
@@ -9369,7 +9350,7 @@ rollup-plugin-terser@^5.1.1:
|
||||
serialize-javascript "^1.7.0"
|
||||
terser "^4.1.0"
|
||||
|
||||
rollup-pluginutils@^2.3.3, rollup-pluginutils@^2.5.0, rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1:
|
||||
rollup-pluginutils@^2.5.0, rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97"
|
||||
integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg==
|
||||
|
||||
Reference in New Issue
Block a user