Skip to content

Commit

Permalink
feat: support vue 2.7 (#247)
Browse files Browse the repository at this point in the history
closes #240
  • Loading branch information
KaelWD authored Jul 5, 2022
1 parent fb6cf15 commit c0dd7cf
Show file tree
Hide file tree
Showing 13 changed files with 615 additions and 575 deletions.
15 changes: 7 additions & 8 deletions dev5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "UNLICENSED",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development node --preserve-symlinks node_modules/.bin/webpack serve --open --hot",
"dev": "cross-env NODE_ENV=development node --preserve-symlinks node_modules/.bin/webpack serve --hot",
"build:dev": "cross-env NODE_ENV=development node --preserve-symlinks node_modules/.bin/webpack --progress",
"build": "cross-env NODE_ENV=production node --preserve-symlinks node_modules/.bin/webpack --progress"
},
Expand All @@ -20,16 +20,15 @@
"sass": "^1.32.4",
"sass-loader": "^10.1.1",
"url-loader": "^4.1.1",
"vue": "^2.6.12",
"vue-loader": "^15.9.6",
"vue": "^2.7.2",
"vue-loader": "^15.10.0",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.12",
"vuetify": "^2.4.2",
"vuetify": "^2.6.6",
"vuetify-loader": "link:../",
"webpack": "^5.15.0",
"webpack": "^5.73.0",
"webpack-bundle-analyzer": "^4.3.0",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.2"
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^3.11.3"
},
"browserslist": "last 2 Chrome versions"
}
19 changes: 13 additions & 6 deletions dev5/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
<template>
<v-app>
<v-container>
<v-card v-ripple>
<v-card v-ripple v-resize="onResize">
<div style="text-align: center">
<v-img src="@/vuetify.png" style="display: inline-flex"></v-img>
</div>
<v-card-text>
<v-text-field></v-text-field>
</v-card-text>
</v-card>
<pre>{{ {
directives: [{
name: "resize"
}]
} }}</pre>
<setup-component />
</v-container>
</v-app>
</template>

<script>
import SetupComponent from './Setup.vue'
const img = require('@/vuetify.png?vuetify-preload')
console.log(img, img.default)
export default {}
export default {
components: {
SetupComponent
}
}
</script>

<docs>
This is the documentation for App.vue
</docs>
9 changes: 9 additions & 0 deletions dev5/src/Setup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<v-card v-ripple>
Setup component
</v-card>
</template>

<script setup>
//
</script>
5 changes: 3 additions & 2 deletions dev5/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const isProd = process.env.NODE_ENV === 'production'
function sassLoaderOptions (indentedSyntax = false) {
return {
implementation: require('sass'),
additionalData: `@import "~@/_variables.scss"` + (indentedSyntax ? '' : ';'),
// additionalData: `@import "~@/_variables.scss"` + (indentedSyntax ? '' : ';'),
sassOptions: { indentedSyntax },
}
}
Expand Down Expand Up @@ -67,7 +67,8 @@ module.exports = {
plugins: [
new VueLoaderPlugin(),
new VuetifyLoaderPlugin({
progressiveImages: true
progressiveImages: true,
registerStylesSSR: false,
}),
new BundleAnalyzerPlugin({
analyzerMode: 'static',
Expand Down
769 changes: 397 additions & 372 deletions dev5/yarn.lock

Large diffs are not rendered by default.

189 changes: 100 additions & 89 deletions lib/loader.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
const path = require('path')
const loaderUtils = require('loader-utils')
const acorn = require('acorn')
const acornWalk = require('acorn-walk')

const vuetifyMatcher = require('./matcher/tag')
const vuetifyAttrsMatcher = require('./matcher/attr')
const { camelize, capitalize, hyphenate, requirePeer } = require('./util')
const runtimePaths = {
installComponents: require.resolve('./runtime/installComponents'),
installDirectives: require.resolve('./runtime/installDirectives')
}

let compiler
try {
compiler = require('vue/compiler-sfc')
} catch (e) {
compiler = require('vue-template-compiler')
}
const { camelize, capitalize, hyphenate } = require('./util')

function getMatches (type, items, matches, component) {
function getMatches (type, items, matches) {
const imports = []

items.forEach(item => {
Expand All @@ -25,7 +15,6 @@ function getMatches (type, items, matches, component) {
[`kebab${type}`]: hyphenate(item),
[`camel${type}`]: capitalize(camelize(item)),
path: this.resourcePath.substring(this.rootContext.length + 1),
component
})
if (match) {
imports.push(match)
Expand All @@ -46,44 +35,22 @@ function injectStylesSSR (imports) {

if (styles.size) {
return `
if (process.env.VUE_ENV === 'server') {
const options = typeof component.exports === 'function'
? component.exports.extendOptions
: component.options
const existing = options.beforeCreate
const hook = function () {
${[...styles].map((style) => ` require('vuetify/${style}').__inject__(this.$ssrContext)`).join('\n')}
}
options.beforeCreate = existing
? [].concat(existing, hook)
: [hook]
}
`
}
return ""
}

function install (install, content, imports, options = {}) {
if (imports.length) {
let newContent = '/* vuetify-loader */\n'
newContent += `import ${install} from ${loaderUtils.stringifyRequest(this, '!' + runtimePaths[install])}\n`
newContent += imports.map(i => i[1]).join('\n') + '\n'
newContent += `${install}(component, {${imports.map(i => i[0]).join(',')}})\n`

if (options.registerStylesSSR) {
newContent += injectStylesSSR(imports, newContent)
}

// Insert our modification before the HMR code
const hotReload = content.indexOf('/* hot reload */')
if (hotReload > -1) {
content = content.slice(0, hotReload) + newContent + '\n\n' + content.slice(hotReload)
} else {
content += '\n\n' + newContent
render._vuetifyStyles = function (component) {
if (process.env.VUE_ENV === 'server') {
const options = typeof component.exports === 'function'
? component.exports.extendOptions
: component.options
const existing = options.beforeCreate
const hook = function () {
${[...styles].map((style) => ` require('vuetify/${style}').__inject__(this.$ssrContext)`).join('\n')}
}
options.beforeCreate = existing
? [].concat(existing, hook)
: [hook]
}

return content
}`
}
return ''
}

module.exports = async function (content, sourceMap) {
Expand All @@ -103,52 +70,96 @@ module.exports = async function (content, sourceMap) {
options.match.push(vuetifyMatcher)
options.attrsMatch.push(vuetifyAttrsMatcher)

if (!this.resourceQuery) {
const readFile = path => new Promise((resolve, reject) => {
this.fs.readFile(path, function (err, data) {
if (err) reject(err)
else resolve(data)
})
})

const qs = new URLSearchParams(this.resourceQuery)
if (qs.has('vue') && qs.get('type') === 'template') {
this.addDependency(this.resourcePath)

const tags = new Set()
const attrs = new Set()
const file = (await readFile(this.resourcePath)).toString('utf8')
const component = compiler.parseComponent(file)
if (component.template) {
if (component.template.src) {
const externalFile = (await new Promise((resolve, reject) =>
this.resolve(path.dirname(this.resourcePath), component.template.src, (err, result) => {
if (err) reject(err)
else resolve(result)
})
))
const externalContent = (await readFile(externalFile)).toString('utf8')
component.template.content = externalContent
const matches = {
components: [],
directives: [],
}

const ast = acorn.parse(content, { sourceType: 'module', ecmaVersion: 'latest' })
acornWalk.simple(ast, {
CallExpression (node) {
if (node.callee.name === '_c') {
if (node.arguments[0].type === 'Literal') {
matches.components.push([node.arguments[0].value, node.arguments[0].start, node.arguments[0].end])
}
if (node.arguments.length >= 2 && node.arguments[1].type === 'ObjectExpression') {
const props = node.arguments[1].properties
props.forEach(prop => {
if (prop.key.type === 'Identifier' && prop.key.name === 'directives' && prop.value.type === 'ArrayExpression') {
prop.value.elements.forEach(directive => {
if (directive.type === 'ObjectExpression') {
directive.properties.forEach(prop => {
if (prop.key.type === 'Identifier' && prop.key.name === 'name') {
matches.directives.push([prop.value.value, prop.start, prop.end])
}
})
}
})
}
})
}
}
}
if (component.template.lang === 'pug') {
const pug = requirePeer('pug')
try {
component.template.content = pug.render(component.template.content, {filename: this.resourcePath})
} catch (err) {/* Ignore compilation errors, they'll be picked up by other loaders */}
})

const components = getMatches.call(this, 'Tag', dedupe(matches.components), options.match)
const directives = getMatches.call(this, 'Attr', dedupe(matches.directives), options.attrsMatch)

const allMatches = [...matches.components.map(v => ({
type: 'component',
name: capitalize(camelize(v[0])),
start: v[1],
end: v[2],
})), ...matches.directives.map(v => ({
type: 'directive',
name: capitalize(camelize(v[0])),
start: v[1],
end: v[2],
}))].sort((a, b) => a.start - b.start)

for (let i = allMatches.length - 1; i >= 0; i--) {
const tag = allMatches[i]
if (tag.type === 'component') {
if (!components.some(c => c[0] === tag.name)) continue
content = content.slice(0, tag.start) + tag.name + content.slice(tag.end)
} else {
if (!directives.some(c => c[0] === tag.name)) continue
const indent = content.slice(0, tag.start).match(/\s*$/)[0]
content = content.slice(0, tag.start) +
'def: ' + tag.name + ',' +
indent + content.slice(tag.start)
}
compiler.compile(component.template.content, {
modules: [{
postTransformNode: node => {
if ("directives" in node) {
node.directives.forEach(({ name }) => attrs.add(name))
}
tags.add(node.tag)
}
}]
})
}

content = install.call(this, 'installComponents', content, getMatches.call(this, 'Tag', tags, options.match, component), options)
content = install.call(this, 'installDirectives', content, getMatches.call(this, 'Attr', attrs, options.attrsMatch, component))
const imports = [...components, ...directives]
if (imports.length) {
content = imports.map(v => v[1]).join('\n') + '\n\n' + content
}

if (options.registerStylesSSR) {
content += injectStylesSSR(imports)
}
} else if (options.registerStylesSSR && !this.resourceQuery) {
this.addDependency(this.resourcePath)
const newContent = 'if (render._vuetifyStyles) { render._vuetifyStyles(component) }'

// Insert our modification before the HMR code
const hotReload = content.indexOf('/* hot reload */')
if (hotReload > -1) {
content = content.slice(0, hotReload) + newContent + '\n\n' + content.slice(hotReload)
} else {
content += '\n\n' + newContent
}
}

this.callback(null, content, sourceMap)
}

function dedupe (matches) {
return [...new Set(matches.map(i => capitalize(camelize(i[0]))))]
}
2 changes: 1 addition & 1 deletion lib/matcher/attr.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { directives } = require('./generator')

module.exports = function match (_, { kebabAttr, camelAttr: attr }) {
if (directives.includes(attr)) return [attr, `import ${attr} from 'vuetify/lib/directives/${kebabAttr}'`]
if (directives.includes(attr)) return [attr, `import ${attr} from 'vuetify/lib/directives/${kebabAttr}';`]
}
2 changes: 1 addition & 1 deletion lib/matcher/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ readdirSync(dir).forEach(group => {
// This is required so that groups picks up dependencies they have to other groups.
// For example VTabs depends on the style from VSlideGroup (VSlideGroup.sass).
// As VSlideGroup will be loaded before (alphabetically), `Module._load` wouldn't be called for it when processing VTabs (as it would be already in the require cache).
// By busting the require cache for each groups we unsure that when loading VTabs we do call `Module._load` for `VSlideGroup.sass` and it gets added to the dependencies.
// By busting the require cache for each groups we ensure that when loading VTabs we do call `Module._load` for `VSlideGroup.sass` and it gets added to the dependencies.
decache(`vuetify/es5/components/${group}`)
})

Expand Down
39 changes: 12 additions & 27 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ class VuetifyLoaderPlugin {
)
}

vueRules.forEach(this.updateVueRule.bind(this))

const rules = [...compiler.options.module.rules]
vueRules.forEach(({ rule, index }) => {
rules[index] = rule
compiler.options.module.rules.unshift({
test: /\.vue$/,
use: {
loader: require.resolve('./loader'),
options: {
match: this.options.match || [],
attrsMatch: this.options.attrsMatch || [],
registerStylesSSR: this.options.registerStylesSSR || false
}
},
})
compiler.options.module.rules = rules

vueRules.forEach(this.updateVueRule.bind(this))

if (this.options.progressiveImages) {
const options = typeof this.options.progressiveImages === 'boolean'
Expand Down Expand Up @@ -65,27 +71,6 @@ class VuetifyLoaderPlugin {
vueLoaderOptions.compilerOptions.modules = vueLoaderOptions.compilerOptions.modules || []
vueLoaderOptions.compilerOptions.modules.push(progressiveLoaderModule)
}

rule.oneOf = [
{
resourceQuery: '?',
use: rule.use
},
{
use: [
{
loader: require.resolve('./loader'),
options: {
match: this.options.match || [],
attrsMatch: this.options.attrsMatch || [],
registerStylesSSR: this.options.registerStylesSSR || false
}
},
...rule.use
]
},
]
delete rule.use
}
}

Expand Down
Loading

0 comments on commit c0dd7cf

Please sign in to comment.