Skip to content

Commit

Permalink
feat: apply loaders matching .js to compiled template code
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Dec 18, 2019
1 parent d4f151a commit 20dbbfc
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 145 deletions.
10 changes: 3 additions & 7 deletions src/index.ts
Expand Up @@ -22,8 +22,6 @@ export interface VueLoaderOptions {
compiler?: TemplateCompiler
compilerOptions?: CompilerOptions
hotReload?: boolean
cacheDirectory?: string
cacheIdentifier?: string
exposeFilename?: boolean
appendExtension?: boolean
}
Expand Down Expand Up @@ -61,7 +59,6 @@ const loader: webpack.loader.Loader = function(source) {
} = loaderContext

const rawQuery = resourceQuery.slice(1)
const inheritQuery = `&${rawQuery}`
const incomingQuery = qs.parse(rawQuery)
const options = (loaderUtils.getOptions(loaderContext) ||
{}) as VueLoaderOptions
Expand Down Expand Up @@ -109,7 +106,7 @@ const loader: webpack.loader.Loader = function(source) {
const idQuery = `&id=${id}`
const scopedQuery = hasScoped ? `&scoped=true` : ``
const attrsQuery = attrsToQuery(descriptor.template.attrs)
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}`
templateRequest = stringifyRequest(src + query)
templateImport = `import render from ${templateRequest}`
}
Expand All @@ -119,7 +116,7 @@ const loader: webpack.loader.Loader = function(source) {
if (descriptor.script) {
const src = descriptor.script.src || resourcePath
const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
const query = `?vue&type=script${attrsQuery}${inheritQuery}`
const query = `?vue&type=script${attrsQuery}${resourceQuery}`
const scriptRequest = stringifyRequest(src + query)
scriptImport =
`import script from ${scriptRequest}\n` + `export * from ${scriptRequest}` // support named exports
Expand All @@ -132,11 +129,10 @@ const loader: webpack.loader.Loader = function(source) {
descriptor.styles.forEach((style: SFCStyleBlock, i: number) => {
const src = style.src || resourcePath
const attrsQuery = attrsToQuery(style.attrs, 'css')
const inheritQuery = `&${loaderContext.resourceQuery.slice(1)}`
// make sure to only pass id when necessary so that we don't inject
// duplicate tags when multiple components import the same css file
const idQuery = style.scoped ? `&id=${id}` : ``
const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${inheritQuery}`
const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${resourceQuery}`
const styleRequest = stringifyRequest(src + query)
if (style.module) {
if (!hasCSSModules) {
Expand Down
193 changes: 67 additions & 126 deletions src/pitcher.ts
@@ -1,11 +1,9 @@
import * as webpack from 'webpack'
import qs from 'querystring'
import loaderUtils from 'loader-utils'
import hash from 'hash-sum'
import { VueLoaderOptions } from 'src'

const selfPath = require.resolve('./index')
const templateLoaderPath = require.resolve('./templateLoader')
// const templateLoaderPath = require.resolve('./templateLoader')
const stylePostLoaderPath = require.resolve('./stylePostLoader')

// @types/webpack doesn't provide the typing for loaderContext.loaders...
Expand All @@ -20,161 +18,104 @@ const isESLintLoader = (l: Loader) => /(\/|\\|@)eslint-loader/.test(l.path)
const isNullLoader = (l: Loader) => /(\/|\\|@)null-loader/.test(l.path)
const isCSSLoader = (l: Loader) => /(\/|\\|@)css-loader/.test(l.path)
const isCacheLoader = (l: Loader) => /(\/|\\|@)cache-loader/.test(l.path)
const isPitcher = (l: Loader) => l.path !== __filename
const isPreLoader = (l: Loader) => !l.pitchExecuted
const isPostLoader = (l: Loader) => l.pitchExecuted

const dedupeESLintLoader = (loaders: Loader[]) => {
const res: Loader[] = []
let seen = false
loaders.forEach((l: Loader) => {
if (!isESLintLoader(l)) {
res.push(l)
} else if (!seen) {
seen = true
res.push(l)
}
})
return res
}

const shouldIgnoreCustomBlock = (loaders: Loader[]) => {
const actualLoaders = loaders.filter(loader => {
// vue-loader
if (loader.path === selfPath) {
return false
}

// cache-loader
if (isCacheLoader(loader)) {
return false
}

return true
})
return actualLoaders.length === 0
}
const isNotPitcher = (l: Loader) => l.path !== __filename

const pitcher: webpack.loader.Loader = code => code

module.exports = pitcher

// This pitching loader is responsible for intercepting all vue block requests
// and transform it into appropriate requests.
pitcher.pitch = function() {
pitcher.pitch = function(r) {
const context = this as webpack.loader.LoaderContext
const options = loaderUtils.getOptions(context) as VueLoaderOptions
const { cacheDirectory, cacheIdentifier } = options
const query = qs.parse(context.resourceQuery.slice(1))

let loaders = context.loaders

// if this is a language block request, eslint-loader may get matched
// multiple times
if (query.type) {
// if this is an inline block, since the whole file itself is being linted,
// remove eslint-loader to avoid duplicate linting.
if (/\.vue$/.test(context.resourcePath)) {
loaders = loaders.filter((l: Loader) => !isESLintLoader(l))
} else {
// This is a src import. Just make sure there's not more than 1 instance
// of eslint present.
loaders = dedupeESLintLoader(loaders)
}
}

// remove self
loaders = loaders.filter(isPitcher)
const rawLoaders = context.loaders.filter(isNotPitcher)
let loaders = rawLoaders

// do not inject if user uses null-loader to void the type (#1239)
if (loaders.some(isNullLoader)) {
return
}

const genRequest = (loaders: Loader[]) => {
// Important: dedupe since both the original rule
// and the cloned rule would match a source import request.
// also make sure to dedupe based on loader path.
// assumes you'd probably never want to apply the same loader on the same
// file twice.
// Exception: in Vue CLI we do need two instances of postcss-loader
// for user config and inline minification. So we need to dedupe baesd on
// path AND query to be safe.
const seen = new Map()
const loaderStrings: string[] = []

loaders.forEach(loader => {
const identifier = typeof loader === 'string'
? loader
: (loader.path + loader.query)
const request = typeof loader === 'string' ? loader : loader.request
if (!seen.has(identifier)) {
seen.set(identifier, true)
// loader.request contains both the resolved loader path and its options
// query (e.g. ??ref-0)
loaderStrings.push(request)
}
})

return loaderUtils.stringifyRequest(context, '-!' + [
...loaderStrings,
context.resourcePath + context.resourceQuery
].join('!'))
const query = qs.parse(context.resourceQuery.slice(1))
const isInlineBlock = /\.vue$/.test(context.resourcePath)
// eslint-loader may get matched multiple times
// if this is an inline block, since the whole file itself is being linted,
// remove eslint-loader to avoid duplicate linting.
if (isInlineBlock) {
loaders = loaders.filter((l: Loader) => !isESLintLoader(l))
}

// Important: dedupe loaders since both the original rule
// and the cloned rule would match a source import request or a
// resourceQuery-only rule that intends to target a custom block with no lang
const seen = new Map()
loaders = loaders.filter(loader => {
const identifier = typeof loader === 'string'
? loader
// Dedupe based on both path and query if available. This is important
// in Vue CLI so that postcss-loaders with different options can co-exist
: (loader.path + loader.query)
if (!seen.has(identifier)) {
seen.set(identifier, true)
return true
}
})

// Inject style-post-loader before css-loader for scoped CSS and trimming
if (query.type === `style`) {
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
if (cssLoaderIndex > -1) {
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
const request = genRequest([
return genProxyModule([
...afterLoaders,
stylePostLoaderPath,
...beforeLoaders
])
// console.log(request)
return `import mod from ${request}; export default mod; export * from ${request}`
], context)
}
}

// for templates: inject the template compiler & optional cache
if (query.type === `template`) {
const path = require('path')
const cacheLoader = cacheDirectory && cacheIdentifier
? [`${require.resolve('cache-loader')}?${JSON.stringify({
// For some reason, webpack fails to generate consistent hash if we
// use absolute paths here, even though the path is only used in a
// comment. For now we have to ensure cacheDirectory is a relative path.
cacheDirectory: (path.isAbsolute(cacheDirectory)
? path.relative(process.cwd(), cacheDirectory)
: cacheDirectory).replace(/\\/g, '/'),
cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template'
})}`]
: []

const preLoaders = loaders.filter(isPreLoader)
const postLoaders = loaders.filter(isPostLoader)

const request = genRequest([
...cacheLoader,
...postLoaders,
templateLoaderPath + `??vue-loader-options`,
...preLoaders
])
// console.log(request)
return `import mod from ${request}; export default mod;`
}

// if a custom block has no other matching loader other than vue-loader itself
// or cache-loader, we should ignore it
if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
return ``
}

// When the user defines a rule that has only resourceQuery but no test,
// both that rule and the cloned rule will match, resulting in duplicated
// loaders. Therefore it is necessary to perform a dedupe here.
const request = genRequest(loaders)
return `import mod from ${request}; export default mod; export * from ${request}`
// rewrite if we have deduped loaders
if (loaders.length !== rawLoaders.length) {
return genProxyModule(loaders, context)
}
}

function genProxyModule(loaders: Loader[], context: webpack.loader.LoaderContext) {
const loaderStrings = loaders.map(loader => {
return typeof loader === 'string' ? loader : loader.request
})
const resource = context.resourcePath + context.resourceQuery
const request = loaderUtils.stringifyRequest(context, '-!' + [
...loaderStrings,
resource
].join('!'))
// return a proxy module which simply re-exports everything from the
// actual request.
return (
`import mod from ${request};` +
`export default mod;` +
`export * from ${request}`
)
}

function shouldIgnoreCustomBlock(loaders: Loader[]) {
const actualLoaders = loaders.filter(loader => {
// vue-loader
if (loader.path === selfPath) {
return false
}
// cache-loader
if (isCacheLoader(loader)) {
return false
}
return true
})
return actualLoaders.length === 0
}
55 changes: 43 additions & 12 deletions src/plugin.ts
Expand Up @@ -55,11 +55,7 @@ class VueLoaderPlugin implements webpack.Plugin {
)
}

// make sure vue-loader options has a known ident so that we can share
// options by reference in the template-loader by using a ref query like
// template-loader??vue-loader-options
const vueLoaderUse = vueUse[vueLoaderUseIndex]
vueLoaderUse.ident = 'vue-loader-options'
const vueLoaderOptions = (vueLoaderUse.options = vueLoaderUse.options || {}) as VueLoaderOptions

// for each user rule (expect the vue rule), create a cloned rule
Expand All @@ -68,23 +64,38 @@ class VueLoaderPlugin implements webpack.Plugin {
.filter(r => r !== vueRule)
.map(cloneRule)

// global pitcher (responsible for injecting template compiler loader & CSS
// post loader)
// rule for template compiler
const templateCompilerRule = {
loader: require.resolve('./templateLoader'),
test: /\.vue$/,
resourceQuery: isVueTemplateBlock,
options: vueLoaderOptions
}

// for each rule that matches plain .js files, also create a clone and
// match it against the compiled template code inside *.vue files, so that
// compiled vue render functions receive the same treatment as user code
// (mostly babel)
const matchesJS = createMatcher(`test.js`)
const jsRulesForRenderFn = rules
.filter(r => r !== vueRule && matchesJS(r))
.map(cloneRuleForRenderFn)

// pitcher for block requests (for injecting stylePostLoader and deduping
// loaders matched for src imports)
const pitcher = {
loader: require.resolve('./pitcher'),
resourceQuery: (query: string) => {
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
cacheDirectory: vueLoaderOptions.cacheDirectory,
cacheIdentifier: vueLoaderOptions.cacheIdentifier
}
}

// replace original rules
compiler.options.module!.rules = [
pitcher,
...jsRulesForRenderFn,
templateCompilerRule,
...clonedRules,
...rules
]
Expand Down Expand Up @@ -114,7 +125,8 @@ function cloneRule (rule: webpack.RuleSetRule) {
// it in `resourceQuery`. This ensures when we use the normalized rule's
// resource check, include/exclude are matched correctly.
let currentResource: string
const res = Object.assign({}, rule, {
const res = {
...rule,
resource: {
test: (resource: string) => {
currentResource = resource
Expand All @@ -138,7 +150,7 @@ function cloneRule (rule: webpack.RuleSetRule) {
}
return true
}
})
}

if (rule.oneOf) {
res.oneOf = rule.oneOf.map(cloneRule)
Expand All @@ -147,4 +159,23 @@ function cloneRule (rule: webpack.RuleSetRule) {
return res
}

function isVueTemplateBlock(query: string) {
const parsed = qs.parse(query.slice(1))
return parsed.vue != null && parsed.type === 'template'
}

function cloneRuleForRenderFn(rule: webpack.RuleSetRule) {
const res = {
...rule,
resource: {
test: /\.vue$/
},
resourceQuery: isVueTemplateBlock
}
if (rule.oneOf) {
res.oneOf = rule.oneOf.map(cloneRule)
}
return res
}

module.exports = VueLoaderPlugin

0 comments on commit 20dbbfc

Please sign in to comment.