2
0
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:
pimlie
2019-06-11 14:57:27 +02:00
parent 02237e0176
commit 824b5ab9ef
9 changed files with 372 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
testEnvironment: 'node'
}
+17
View File
@@ -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"
}
}
+91
View File
@@ -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)
}
+21
View File
@@ -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')
})
}
}
+9
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
<head>
<title>Static Title</title>
<meta name="description" content="static description">
</head>
<template>
<div>
<h1>Test</h1>
</div>
</template>
+10
View File
@@ -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()
})
})
+168
View File
@@ -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
}