diff --git a/.circleci/config.yml b/.circleci/config.yml index 49d52e35..433275c3 100755 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -134,5 +134,3 @@ workflows: requires: [test-e2e-ssr] filters: branches: { ignore: /^pull\/.*/ } - - diff --git a/examples/global.css b/examples/_static/global.css similarity index 100% rename from examples/global.css rename to examples/_static/global.css diff --git a/examples/index.html b/examples/_static/index.html similarity index 86% rename from examples/index.html rename to examples/_static/index.html index 282dd821..a1704e20 100644 --- a/examples/index.html +++ b/examples/_static/index.html @@ -11,9 +11,11 @@

Vue Meta Examples

  • Basic Render
  • Keep alive
  • Usage with multiple apps
  • +
  • SSR
  • Usage with vue-router
  • Usage with vuex
  • Usage with vuex + async actions
  • +
  • Async Callback
  • diff --git a/examples/_static/user-1.js b/examples/_static/user-1.js new file mode 100644 index 00000000..5214b8ba --- /dev/null +++ b/examples/_static/user-1.js @@ -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' + } +}) diff --git a/examples/_static/user-2.js b/examples/_static/user-2.js new file mode 100644 index 00000000..38d78342 --- /dev/null +++ b/examples/_static/user-2.js @@ -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' + } +}) diff --git a/examples/_static/user-3.js b/examples/_static/user-3.js new file mode 100644 index 00000000..9d9499b1 --- /dev/null +++ b/examples/_static/user-3.js @@ -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' + } +}) diff --git a/examples/_static/user-4.js b/examples/_static/user-4.js new file mode 100644 index 00000000..b74d06b0 --- /dev/null +++ b/examples/_static/user-4.js @@ -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' + } +}) diff --git a/examples/async-callback/app.js b/examples/async-callback/app.js new file mode 100644 index 00000000..ae2a1af0 --- /dev/null +++ b/examples/async-callback/app.js @@ -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: ` +
    +

    Async Callback

    +

    {{ count }} scripts loaded

    + +
    +

    Users

    + +
    +
    + ` +}).$mount('#app') diff --git a/examples/async-callback/index.html b/examples/async-callback/index.html new file mode 100644 index 00000000..af015e01 --- /dev/null +++ b/examples/async-callback/index.html @@ -0,0 +1,13 @@ + + + + Async Callback Title + + + + ← Examples index +
    + + + + diff --git a/examples/basic-render/app.js b/examples/basic-render/app.js index ed3ee11a..35a3bf85 100644 --- a/examples/basic-render/app.js +++ b/examples/basic-render/app.js @@ -11,10 +11,10 @@ Vue.component('child', { default: '' } }, - render(h) { + render (h) { return h('h3', null, this.page) }, - metaInfo() { + metaInfo () { return { title: this.page } diff --git a/examples/keep-alive/app.js b/examples/keep-alive/app.js index 7f95c291..c0f895e4 100644 --- a/examples/keep-alive/app.js +++ b/examples/keep-alive/app.js @@ -11,11 +11,11 @@ Vue.component('foo', { }) new Vue({ - data() { + data () { return { showFoo: false } }, methods: { - show() { + show () { this.showFoo = !this.showFoo } }, diff --git a/examples/multiple-apps/app.js b/examples/multiple-apps/app.js index ed50e8d7..b5a6274f 100644 --- a/examples/multiple-apps/app.js +++ b/examples/multiple-apps/app.js @@ -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: `
    app 4
    ` }) - const app4 = new App().$mount() + new App().$mount() }, 10000) diff --git a/examples/server.js b/examples/server.js index f302f87b..fac6d742 100644 --- a/examples/server.js +++ b/examples/server.js @@ -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 diff --git a/examples/ssr/App.js b/examples/ssr/App.js new file mode 100644 index 00000000..808b49b3 --- /dev/null +++ b/examples/ssr/App.js @@ -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: '

    Hello World

    ', + 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: ` +
    + + +

    {{ count }} users loaded

    + + +
    ` + }) +} diff --git a/examples/ssr/app.js b/examples/ssr/app.js deleted file mode 100644 index b7ee4800..00000000 --- a/examples/ssr/app.js +++ /dev/null @@ -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: '

    Hello

    ', - metaInfo: { - title: 'Coucou', - meta: [ - { - hid: 'description', - name: 'description', - content: 'Coucou' - } - ] - } - } - }, - template: '', - 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'] - } - } - }) -} diff --git a/examples/ssr/app.template.html b/examples/ssr/app.template.html index 531cf99e..396a873d 100644 --- a/examples/ssr/app.template.html +++ b/examples/ssr/app.template.html @@ -1,16 +1,22 @@ - + - {{ meta.text() }} {{ title.text() }} + {{ meta.text() }} + {{ link.text() }} {{ style.text() }} - {{ webpackAssets }} {{ script.text() }} {{ noscript.text() }} + {{ script.text({ pbody: true }) }} + {{ noscript.text({ pbody: true }) }} + + ← Examples index {{ app }} + + {{ script.text({ body: true }) }} {{ noscript.text({ body: true }) }} diff --git a/examples/ssr/browser.js b/examples/ssr/browser.js new file mode 100644 index 00000000..b10d0e5e --- /dev/null +++ b/examples/ssr/browser.js @@ -0,0 +1,5 @@ +import createApp from './App' + +window.users = [] + +createApp().$mount('#app') diff --git a/examples/ssr/client-entry.js b/examples/ssr/client-entry.js deleted file mode 100644 index 0fbbbe4b..00000000 --- a/examples/ssr/client-entry.js +++ /dev/null @@ -1,3 +0,0 @@ -import createApp from './app' - -createApp().$mount('#app') diff --git a/examples/ssr/index.js b/examples/ssr/index.js deleted file mode 100644 index 12f72265..00000000 --- a/examples/ssr/index.js +++ /dev/null @@ -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 = '' - 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)) diff --git a/examples/ssr/server-entry.js b/examples/ssr/server-entry.js deleted file mode 100644 index 1192635c..00000000 --- a/examples/ssr/server-entry.js +++ /dev/null @@ -1,3 +0,0 @@ -import createApp from './app' - -export default createApp diff --git a/examples/ssr/server.js b/examples/ssr/server.js new file mode 100644 index 00000000..2d1964fb --- /dev/null +++ b/examples/ssr/server.js @@ -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 +} diff --git a/examples/vue-router/app.js b/examples/vue-router/app.js index 9dfadd20..4592677f 100644 --- a/examples/vue-router/app.js +++ b/examples/vue-router/app.js @@ -15,21 +15,21 @@ const ChildComponent = {

    You're looking at the {{ page }} page

    Has metaInfo been updated? {{ metaUpdated }}

    `, - 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 } }) diff --git a/examples/vuex-async/store.js b/examples/vuex-async/store.js index 07cdfd5f..453d3d96 100644 --- a/examples/vuex-async/store.js +++ b/examples/vuex-async/store.js @@ -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) diff --git a/examples/vuex/store.js b/examples/vuex/store.js index a3da819a..e2361d3f 100644 --- a/examples/vuex/store.js +++ b/examples/vuex/store.js @@ -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) } } diff --git a/examples/webpack.config.js b/examples/webpack.config.js index 7a8e3060..e43ceda2 100644 --- a/examples/webpack.config.js +++ b/examples/webpack.config.js @@ -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' } ] }, diff --git a/package.json b/package.json index 51cf017d..a8813f2a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/rollup.config.js b/scripts/rollup.config.js index 590989dc..70a2f18c 100644 --- a/scripts/rollup.config.js +++ b/scripts/rollup.config.js @@ -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) }, diff --git a/src/client/$meta.js b/src/client/$meta.js index a18c980c..8d2a121b 100644 --- a/src/client/$meta.js +++ b/src/client/$meta.js @@ -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' diff --git a/src/client/load.js b/src/client/load.js new file mode 100644 index 00000000..d24c202d --- /dev/null +++ b/src/client/load.js @@ -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) + } + } + } +} diff --git a/src/client/updateClientMetaInfo.js b/src/client/updateClientMetaInfo.js index 140a494f..330b1525 100644 --- a/src/client/updateClientMetaInfo.js +++ b/src/client/updateClientMetaInfo.js @@ -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 } diff --git a/src/client/updaters/tag.js b/src/client/updaters/tag.js index ab6a3d19..d51533f6 100644 --- a/src/client/updaters/tag.js +++ b/src/client/updaters/tag.js @@ -10,7 +10,9 @@ import { queryElements, getElementsKey } from '../../utils/elements.js' * @param {(Array|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 - } + /* istanbul ignore next */ + if (!tag.hasOwnProperty(attr)) { + 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 === '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 + if (attr === 'callback') { + newElement.onload = () => tag[attr](newElement) + continue + } - const isBooleanAttribute = includes(booleanHtmlAttributes, attr) - if (isBooleanAttribute && !tag[attr]) { - continue - } + const _attr = includes(dataAttributes, attr) + ? `data-${attr}` + : attr - const value = isBooleanAttribute ? '' : tag[attr] - newElement.setAttribute(_attr, value) + const isBooleanAttribute = includes(booleanHtmlAttributes, attr) + if (isBooleanAttribute && !tag[attr]) { + continue } + + const value = isBooleanAttribute ? '' : tag[attr] + newElement.setAttribute(_attr, value) } const oldElements = currentElements[getElementsKey(tag)] diff --git a/src/server/generators/tag.js b/src/server/generators/tag.js index 7994673b..fcb315da 100644 --- a/src/server/generators/tag.js +++ b/src/server/generators/tag.js @@ -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 (attr === 'callback') { + attrs += ` onload="this.__vm_l=1"` + continue + } + + const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr) if (isBooleanAttr && !tag[attr]) { - return attrsStr + continue } - return isBooleanAttr - ? `${attrsStr} ${prefix}${attr}` - : `${attrsStr} ${prefix}${attr}="${tag[attr]}"` - }, '') + 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}` + return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` + + (hasContent ? `${content}` : '') }, '') } } diff --git a/src/shared/constants.js b/src/shared/constants.js index cb472a28..a347b246 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -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') diff --git a/src/shared/log.js b/src/shared/log.js new file mode 100644 index 00000000..5df54aa2 --- /dev/null +++ b/src/shared/log.js @@ -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') diff --git a/src/shared/merge.js b/src/shared/merge.js index 70158074..23dbe698 100644 --- a/src/shared/merge.js +++ b/src/shared/merge.js @@ -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] } diff --git a/src/shared/mixin.js b/src/shared/mixin.js index c95b5001..7ff37e2d 100644 --- a/src/shared/mixin.js +++ b/src/shared/mixin.js @@ -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) diff --git a/test/e2e/browser.test.js b/test/e2e/browser.test.js index fb8ce144..26cc512b 100644 --- a/test/e2e/browser.test.js +++ b/test/e2e/browser.test.js @@ -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 () => { diff --git a/test/fixtures/app.template.html b/test/fixtures/app.template.html index a475f3c5..3b371f03 100644 --- a/test/fixtures/app.template.html +++ b/test/fixtures/app.template.html @@ -5,7 +5,7 @@ {{ title.text() }} {{ link.text() }} {{ style.text() }} - {{ webpackAssets }} + {{ headAssets }} {{ script.text() }} {{ noscript.text() }} @@ -13,6 +13,7 @@ {{ script.text({ pbody: true }) }} {{ noscript.text({ pbody: true }) }} {{ app }} + {{ bodyAssets }} {{ script.text({ body: true }) }} {{ noscript.text({ body: true }) }} diff --git a/test/fixtures/basic/static/load-test.js b/test/fixtures/basic/static/load-test.js new file mode 100644 index 00000000..03a28f66 --- /dev/null +++ b/test/fixtures/basic/static/load-test.js @@ -0,0 +1 @@ +window.loadTest = 'loaded' diff --git a/test/fixtures/basic/views/home.vue b/test/fixtures/basic/views/home.vue index 52db7740..02b13b3a 100644 --- a/test/fixtures/basic/views/home.vue +++ b/test/fixtures/basic/views/home.vue @@ -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' }, diff --git a/test/unit/generators.test.js b/test/unit/generators.test.js index 8ef8c7d4..eb4b9058 100644 --- a/test/unit/generators.test.js +++ b/test/unit/generators.test.js @@ -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('') + expect(scriptTags.text({ pbody: true })).toBe('') }) 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('') + expect(scriptTags.text({ body: true })).toBe('') expect(scriptTags.text({ pbody: true })).toBe('') }) }) diff --git a/test/unit/load.test.js b/test/unit/load.test.js new file mode 100644 index 00000000..0f7718ac --- /dev/null +++ b/test/unit/load.test.js @@ -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) + }) +}) diff --git a/test/unit/updaters.test.js b/test/unit/updaters.test.js index 6720f37b..d6c9ac2b 100644 --- a/test/unit/updaters.test.js +++ b/test/unit/updaters.test.js @@ -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) }) }) diff --git a/test/utils/browser.js b/test/utils/browser.js deleted file mode 100644 index 197efc70..00000000 --- a/test/utils/browser.js +++ /dev/null @@ -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 - } -} diff --git a/test/utils/build.js b/test/utils/build.js index b145fc39..6da79983 100644 --- a/test/utils/build.js +++ b/test/utils/build.js @@ -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}\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}\n`, '') + + const bodyAssets = assets + .filter(asset => !asset.name.includes('chunk')) + .reduce((s, asset) => `${s}\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: { diff --git a/test/utils/chrome.js b/test/utils/chrome.js deleted file mode 100644 index 96066aab..00000000 --- a/test/utils/chrome.js +++ /dev/null @@ -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 - } -} diff --git a/test/utils/index.js b/test/utils/index.js index 76f916f2..300cb965 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -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 = '', options = {}) { + const dom = new JSDOM(html, options) + + return { + dom, + window: dom.window, + document: dom.window.document + } +} diff --git a/test/utils/meta-info-data.js b/test/utils/meta-info-data.js index a82a5293..82e42764 100644 --- a/test/utils/meta-info-data.js +++ b/test/utils/meta-info-data.js @@ -26,11 +26,11 @@ const metaInfoData = { base: { add: { data: [{ href: 'href' }], - expect: [''] + expect: [''] }, change: { data: [{ href: 'href2' }], - expect: [''] + expect: [''] }, remove: { data: [], @@ -41,8 +41,8 @@ const metaInfoData = { add: { data: [{ charset: 'utf-8' }, { property: 'a', content: 'a' }], expect: [ - '', - '' + '', + '' ] }, change: { @@ -51,8 +51,8 @@ const metaInfoData = { { property: 'a', content: 'b' } ], expect: [ - '', - '' + '', + '' ] }, // make sure elements that already exists are not unnecessarily updated @@ -62,8 +62,8 @@ const metaInfoData = { { property: 'a', content: 'c' } ], expect: [ - '', - '' + '', + '' ], test (side, defaultTest) { if (side === 'client') { @@ -85,11 +85,11 @@ const metaInfoData = { link: { add: { data: [{ rel: 'stylesheet', href: 'href' }], - expect: [''] + expect: [''] }, change: { data: [{ rel: 'stylesheet', href: 'href', media: 'screen' }], - expect: [''] + expect: [''] }, remove: { data: [], @@ -99,11 +99,11 @@ const metaInfoData = { style: { add: { data: [{ type: 'text/css', cssText: '.foo { color: red; }' }], - expect: [''] + expect: [''] }, change: { data: [{ type: 'text/css', cssText: '.foo { color: blue; }' }], - expect: [''] + expect: [''] }, 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: [ - '', - '', - '' + '', + '', + '' ], 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: [''] + expect: [''] }, remove: { data: [], @@ -160,11 +162,11 @@ const metaInfoData = { noscript: { add: { data: [{ innerHTML: '

    noscript

    ' }], - expect: [''] + expect: [''] }, change: { data: [{ innerHTML: '

    noscript, no really

    ' }], - expect: [''] + expect: [''] }, remove: { data: [], diff --git a/yarn.lock b/yarn.lock index d447e22a..f40f1b20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==