mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-13 16:42:25 +03:00
feat: first commit for vue-meta webpack loader
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node'
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@vue-meta/webpack-loader",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^24.8.0",
|
||||
"jsdom": "^15.1.1",
|
||||
"memory-fs": "^0.4.1",
|
||||
"vue": "^2.6.10",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"webpack": "^4.33.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
const qs = require('querystring')
|
||||
const { compile } = require('vue-template-compiler')
|
||||
|
||||
function vnodesToMetaData(vnodes, metaInfo = {}) {
|
||||
let hasBindings = false
|
||||
for (const vnode of vnodes) {
|
||||
if (vnode.type !== 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!metaInfo[vnode.tag]) {
|
||||
metaInfo[vnode.tag] = []
|
||||
}
|
||||
|
||||
const metaData = {}
|
||||
if (vnode.attrsList.length) {
|
||||
vnode.attrs.forEach(attr => (metaData[attr.name] = `${attr.dynamic === undefined ? '' : 'this.'}${attr.value}`))
|
||||
}
|
||||
|
||||
if (vnode.children.length) {
|
||||
metaData.innerHTML = vnode.children.reduce((acc, child) => {
|
||||
if (child.static) {
|
||||
return `"${child.text}"`
|
||||
}
|
||||
|
||||
return acc + child.tokens.reduce((bcc, token) => {
|
||||
if (typeof token === 'object' && token['@binding']) {
|
||||
hasBindings = true
|
||||
return `${bcc}${bcc ? ' + ' : ''}this.${token['@binding']}`
|
||||
}
|
||||
|
||||
return `${bcc}${bcc ? ' + ' : ''}"${token}"`
|
||||
}, '')
|
||||
}, '')
|
||||
}
|
||||
|
||||
metaInfo[vnode.tag].push(metaData)
|
||||
}
|
||||
|
||||
return [
|
||||
metaInfo,
|
||||
hasBindings
|
||||
]
|
||||
}
|
||||
|
||||
function templify(value) {
|
||||
if (Array.isArray(value)) {
|
||||
const arrStr = value.map(templify).join(',')
|
||||
return `[${arrStr}]`
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const objStr = Object.keys(value).map(key => `"${key}": ${templify(value[key])}`).join(',')
|
||||
return `{${objStr}}`
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
module.exports = function (source, map) {
|
||||
const rawQuery = this.resourceQuery.slice(1)
|
||||
const inheritQuery = `&${rawQuery}`
|
||||
const incomingQuery = qs.parse(rawQuery)
|
||||
|
||||
if (incomingQuery.type !== 'custom' || incomingQuery.blockType !== 'head') {
|
||||
this.callback(null, source, map)
|
||||
}
|
||||
|
||||
const vnodes = compile(`<head>${source}</head>`)
|
||||
|
||||
const [ metaInfo, hasBindings ] = vnodesToMetaData(vnodes.ast.children)
|
||||
|
||||
let content
|
||||
if (hasBindings) {
|
||||
content = `
|
||||
Component.options.computed = {
|
||||
...Component.options.computed,
|
||||
metaInfo() {
|
||||
return ${templify(metaInfo)}
|
||||
}
|
||||
}`
|
||||
} else {
|
||||
content = `
|
||||
Component.options.data = {
|
||||
...Component.options.data,
|
||||
metaInfo: ${templify(metaInfo)}
|
||||
}`
|
||||
}
|
||||
|
||||
this.callback(null, `export default function (Component) { ${content} }`, map)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
module.exports = class VueMetaLoaderPlugin {
|
||||
apply(compiler) {
|
||||
const rules = compiler.options.module.rules
|
||||
const vueRule = rules.find(r => r.loader.includes('vue-loader'))
|
||||
|
||||
if (!vueRule) {
|
||||
console.error('vue-loader not found in webpack config')
|
||||
return
|
||||
}
|
||||
|
||||
if (vueRule.use) {
|
||||
console.error('vue-meta-loader plugin should be added before the vue-loader plugin')
|
||||
return
|
||||
}
|
||||
|
||||
compiler.options.module.rules.push({
|
||||
resourceQuery: /blockType=head/,
|
||||
loader: require.resolve('./loader.js')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import Component from '~target'
|
||||
import * as exports from '~target'
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.module = Component
|
||||
window.exports = exports
|
||||
}
|
||||
|
||||
export default Component
|
||||
@@ -0,0 +1,10 @@
|
||||
<head>
|
||||
<title>Static Title</title>
|
||||
<meta name="description" content="static description">
|
||||
</head>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Test</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<meta key="description" name="description" :content="description">
|
||||
</head>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Test</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
const path = require('path')
|
||||
|
||||
const {
|
||||
mockRender,
|
||||
mockBundleAndRun
|
||||
} = require('./utils')
|
||||
|
||||
test('static metadata', (done) => {
|
||||
mockBundleAndRun({
|
||||
entry: 'static.vue'
|
||||
}, ({ window, module }) => {
|
||||
expect(module.data.metaInfo).toBeDefined()
|
||||
|
||||
const vnode = mockRender(module)
|
||||
|
||||
expect(vnode.metaInfo.title).toBeDefined()
|
||||
expect(vnode.metaInfo.title[0].innerHTML).toBe('Static Title')
|
||||
expect(vnode.metaInfo.meta).toBeDefined()
|
||||
expect(vnode.metaInfo.meta[0].name).toBe('description')
|
||||
expect(vnode.metaInfo.meta[0].content).toBe('static description')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('metadata with bindings', (done) => {
|
||||
mockBundleAndRun({
|
||||
entry: 'with-bindings.vue'
|
||||
}, ({ window, module }) => {
|
||||
expect(module.computed.metaInfo).not.toBeUndefined()
|
||||
|
||||
const vnode = mockRender(module, {
|
||||
title: 'Test Title',
|
||||
description: 'Test Description'
|
||||
})
|
||||
|
||||
expect(vnode.metaInfo.title).toBeDefined()
|
||||
expect(vnode.metaInfo.title[0].innerHTML).toBe('Test Title')
|
||||
expect(vnode.metaInfo.meta).toBeDefined()
|
||||
expect(vnode.metaInfo.meta[0].name).toBe('description')
|
||||
expect(vnode.metaInfo.meta[0].content).toBe('Test Description')
|
||||
done()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,168 @@
|
||||
const path = require('path')
|
||||
const Vue = require('vue')
|
||||
const hash = require('hash-sum')
|
||||
const { JSDOM, VirtualConsole } = require('jsdom')
|
||||
const webpack = require('webpack')
|
||||
const merge = require('webpack-merge')
|
||||
const MemoryFS = require('memory-fs')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
|
||||
const mfs = new MemoryFS()
|
||||
const VueMetaLoaderPlugin = require('../src/plugin')
|
||||
|
||||
const baseConfig = {
|
||||
mode: 'development',
|
||||
devtool: false,
|
||||
output: {
|
||||
path: '/',
|
||||
filename: 'test.build.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new VueMetaLoaderPlugin(),
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.optimize.ModuleConcatenationPlugin()
|
||||
]
|
||||
}
|
||||
|
||||
function genId(file) {
|
||||
return hash(path.join('test', 'fixtures', file).replace(/\\/g, '/'))
|
||||
}
|
||||
|
||||
function bundle(options, cb, wontThrowError) {
|
||||
let config = merge({}, baseConfig, options)
|
||||
|
||||
if (config.vue) {
|
||||
const vueOptions = options.vue
|
||||
delete config.vue
|
||||
const vueIndex = config.module.rules.findIndex(r => r.test.test('.vue'))
|
||||
const vueRule = config.module.rules[vueIndex]
|
||||
config.module.rules[vueIndex] = Object.assign({}, vueRule, { options: vueOptions })
|
||||
}
|
||||
|
||||
if (/\.vue/.test(config.entry)) {
|
||||
const vueFile = config.entry
|
||||
config = merge(config, {
|
||||
entry: require.resolve('./fixtures/entry'),
|
||||
resolve: {
|
||||
alias: {
|
||||
'~target': path.resolve(__dirname, './fixtures', vueFile)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (options.modify) {
|
||||
delete config.modify
|
||||
options.modify(config)
|
||||
}
|
||||
|
||||
const webpackCompiler = webpack(config)
|
||||
webpackCompiler.outputFileSystem = mfs
|
||||
webpackCompiler.run((err, stats) => {
|
||||
const errors = stats.compilation.errors
|
||||
if (!wontThrowError) {
|
||||
expect(err).toBeNull()
|
||||
if (errors && errors.length) {
|
||||
errors.forEach((error) => {
|
||||
console.error(error.message)
|
||||
})
|
||||
}
|
||||
expect(errors).toHaveLength(0)
|
||||
}
|
||||
cb(mfs.readFileSync('/test.build.js').toString(), stats, err)
|
||||
})
|
||||
}
|
||||
|
||||
function mockBundleAndRun(options, assert, wontThrowError) {
|
||||
const { suppressJSDOMConsole = true } = options
|
||||
delete options.suppressJSDOMConsole
|
||||
bundle(options, (code, bundleStats, bundleError) => {
|
||||
let dom, jsdomError
|
||||
try {
|
||||
dom = new JSDOM(`<!DOCTYPE html><html><head></head><body></body></html>`, {
|
||||
runScripts: 'outside-only',
|
||||
virtualConsole: suppressJSDOMConsole ? new VirtualConsole() : null
|
||||
})
|
||||
dom.window.eval(code)
|
||||
} catch (e) {
|
||||
console.error(`JSDOM error:\n${e.stack}`)
|
||||
jsdomError = e
|
||||
}
|
||||
|
||||
const { window } = dom
|
||||
const { module, exports } = window
|
||||
const instance = {}
|
||||
if (module && module.beforeCreate) {
|
||||
module.beforeCreate.forEach(hook => hook.call(instance))
|
||||
}
|
||||
assert({
|
||||
window,
|
||||
module,
|
||||
exports,
|
||||
instance,
|
||||
code,
|
||||
jsdomError,
|
||||
bundleStats,
|
||||
bundleError
|
||||
})
|
||||
}, wontThrowError)
|
||||
}
|
||||
|
||||
function mockRender(options, extraData = {}) {
|
||||
const vm = new Vue({
|
||||
...options,
|
||||
data() {
|
||||
return {
|
||||
...(typeof options.data === 'function' ? options.data() : options.data),
|
||||
...extraData
|
||||
}
|
||||
}
|
||||
})
|
||||
vm.$mount()
|
||||
return vm
|
||||
}
|
||||
|
||||
function interopDefault(module) {
|
||||
return module
|
||||
? module.default ? module.default : module
|
||||
: module
|
||||
}
|
||||
|
||||
function initStylesForAllSubComponents(module) {
|
||||
if (module.components) {
|
||||
for (const name in module.components) {
|
||||
const sub = module.components[name]
|
||||
const instance = {}
|
||||
if (sub && sub.beforeCreate) {
|
||||
sub.beforeCreate.forEach(hook => hook.call(instance))
|
||||
}
|
||||
initStylesForAllSubComponents(sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mfs,
|
||||
baseConfig,
|
||||
genId,
|
||||
bundle,
|
||||
mockBundleAndRun,
|
||||
mockRender,
|
||||
interopDefault,
|
||||
initStylesForAllSubComponents
|
||||
}
|
||||
Reference in New Issue
Block a user