Skip to content

Commit 20dbbfc

Browse files
committed
feat: apply loaders matching .js to compiled template code
1 parent d4f151a commit 20dbbfc

File tree

3 files changed

+113
-145
lines changed

3 files changed

+113
-145
lines changed

src/index.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ export interface VueLoaderOptions {
2222
compiler?: TemplateCompiler
2323
compilerOptions?: CompilerOptions
2424
hotReload?: boolean
25-
cacheDirectory?: string
26-
cacheIdentifier?: string
2725
exposeFilename?: boolean
2826
appendExtension?: boolean
2927
}
@@ -61,7 +59,6 @@ const loader: webpack.loader.Loader = function(source) {
6159
} = loaderContext
6260

6361
const rawQuery = resourceQuery.slice(1)
64-
const inheritQuery = `&${rawQuery}`
6562
const incomingQuery = qs.parse(rawQuery)
6663
const options = (loaderUtils.getOptions(loaderContext) ||
6764
{}) as VueLoaderOptions
@@ -109,7 +106,7 @@ const loader: webpack.loader.Loader = function(source) {
109106
const idQuery = `&id=${id}`
110107
const scopedQuery = hasScoped ? `&scoped=true` : ``
111108
const attrsQuery = attrsToQuery(descriptor.template.attrs)
112-
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
109+
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}`
113110
templateRequest = stringifyRequest(src + query)
114111
templateImport = `import render from ${templateRequest}`
115112
}
@@ -119,7 +116,7 @@ const loader: webpack.loader.Loader = function(source) {
119116
if (descriptor.script) {
120117
const src = descriptor.script.src || resourcePath
121118
const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
122-
const query = `?vue&type=script${attrsQuery}${inheritQuery}`
119+
const query = `?vue&type=script${attrsQuery}${resourceQuery}`
123120
const scriptRequest = stringifyRequest(src + query)
124121
scriptImport =
125122
`import script from ${scriptRequest}\n` + `export * from ${scriptRequest}` // support named exports
@@ -132,11 +129,10 @@ const loader: webpack.loader.Loader = function(source) {
132129
descriptor.styles.forEach((style: SFCStyleBlock, i: number) => {
133130
const src = style.src || resourcePath
134131
const attrsQuery = attrsToQuery(style.attrs, 'css')
135-
const inheritQuery = `&${loaderContext.resourceQuery.slice(1)}`
136132
// make sure to only pass id when necessary so that we don't inject
137133
// duplicate tags when multiple components import the same css file
138134
const idQuery = style.scoped ? `&id=${id}` : ``
139-
const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${inheritQuery}`
135+
const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${resourceQuery}`
140136
const styleRequest = stringifyRequest(src + query)
141137
if (style.module) {
142138
if (!hasCSSModules) {

src/pitcher.ts

+67-126
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import * as webpack from 'webpack'
22
import qs from 'querystring'
33
import loaderUtils from 'loader-utils'
4-
import hash from 'hash-sum'
5-
import { VueLoaderOptions } from 'src'
64

75
const selfPath = require.resolve('./index')
8-
const templateLoaderPath = require.resolve('./templateLoader')
6+
// const templateLoaderPath = require.resolve('./templateLoader')
97
const stylePostLoaderPath = require.resolve('./stylePostLoader')
108

119
// @types/webpack doesn't provide the typing for loaderContext.loaders...
@@ -20,161 +18,104 @@ const isESLintLoader = (l: Loader) => /(\/|\\|@)eslint-loader/.test(l.path)
2018
const isNullLoader = (l: Loader) => /(\/|\\|@)null-loader/.test(l.path)
2119
const isCSSLoader = (l: Loader) => /(\/|\\|@)css-loader/.test(l.path)
2220
const isCacheLoader = (l: Loader) => /(\/|\\|@)cache-loader/.test(l.path)
23-
const isPitcher = (l: Loader) => l.path !== __filename
24-
const isPreLoader = (l: Loader) => !l.pitchExecuted
25-
const isPostLoader = (l: Loader) => l.pitchExecuted
26-
27-
const dedupeESLintLoader = (loaders: Loader[]) => {
28-
const res: Loader[] = []
29-
let seen = false
30-
loaders.forEach((l: Loader) => {
31-
if (!isESLintLoader(l)) {
32-
res.push(l)
33-
} else if (!seen) {
34-
seen = true
35-
res.push(l)
36-
}
37-
})
38-
return res
39-
}
40-
41-
const shouldIgnoreCustomBlock = (loaders: Loader[]) => {
42-
const actualLoaders = loaders.filter(loader => {
43-
// vue-loader
44-
if (loader.path === selfPath) {
45-
return false
46-
}
47-
48-
// cache-loader
49-
if (isCacheLoader(loader)) {
50-
return false
51-
}
52-
53-
return true
54-
})
55-
return actualLoaders.length === 0
56-
}
21+
const isNotPitcher = (l: Loader) => l.path !== __filename
5722

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

6025
module.exports = pitcher
6126

6227
// This pitching loader is responsible for intercepting all vue block requests
6328
// and transform it into appropriate requests.
64-
pitcher.pitch = function() {
29+
pitcher.pitch = function(r) {
6530
const context = this as webpack.loader.LoaderContext
66-
const options = loaderUtils.getOptions(context) as VueLoaderOptions
67-
const { cacheDirectory, cacheIdentifier } = options
68-
const query = qs.parse(context.resourceQuery.slice(1))
69-
70-
let loaders = context.loaders
71-
72-
// if this is a language block request, eslint-loader may get matched
73-
// multiple times
74-
if (query.type) {
75-
// if this is an inline block, since the whole file itself is being linted,
76-
// remove eslint-loader to avoid duplicate linting.
77-
if (/\.vue$/.test(context.resourcePath)) {
78-
loaders = loaders.filter((l: Loader) => !isESLintLoader(l))
79-
} else {
80-
// This is a src import. Just make sure there's not more than 1 instance
81-
// of eslint present.
82-
loaders = dedupeESLintLoader(loaders)
83-
}
84-
}
85-
86-
// remove self
87-
loaders = loaders.filter(isPitcher)
31+
const rawLoaders = context.loaders.filter(isNotPitcher)
32+
let loaders = rawLoaders
8833

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

94-
const genRequest = (loaders: Loader[]) => {
95-
// Important: dedupe since both the original rule
96-
// and the cloned rule would match a source import request.
97-
// also make sure to dedupe based on loader path.
98-
// assumes you'd probably never want to apply the same loader on the same
99-
// file twice.
100-
// Exception: in Vue CLI we do need two instances of postcss-loader
101-
// for user config and inline minification. So we need to dedupe baesd on
102-
// path AND query to be safe.
103-
const seen = new Map()
104-
const loaderStrings: string[] = []
105-
106-
loaders.forEach(loader => {
107-
const identifier = typeof loader === 'string'
108-
? loader
109-
: (loader.path + loader.query)
110-
const request = typeof loader === 'string' ? loader : loader.request
111-
if (!seen.has(identifier)) {
112-
seen.set(identifier, true)
113-
// loader.request contains both the resolved loader path and its options
114-
// query (e.g. ??ref-0)
115-
loaderStrings.push(request)
116-
}
117-
})
118-
119-
return loaderUtils.stringifyRequest(context, '-!' + [
120-
...loaderStrings,
121-
context.resourcePath + context.resourceQuery
122-
].join('!'))
39+
const query = qs.parse(context.resourceQuery.slice(1))
40+
const isInlineBlock = /\.vue$/.test(context.resourcePath)
41+
// eslint-loader may get matched multiple times
42+
// if this is an inline block, since the whole file itself is being linted,
43+
// remove eslint-loader to avoid duplicate linting.
44+
if (isInlineBlock) {
45+
loaders = loaders.filter((l: Loader) => !isESLintLoader(l))
12346
}
12447

48+
// Important: dedupe loaders since both the original rule
49+
// and the cloned rule would match a source import request or a
50+
// resourceQuery-only rule that intends to target a custom block with no lang
51+
const seen = new Map()
52+
loaders = loaders.filter(loader => {
53+
const identifier = typeof loader === 'string'
54+
? loader
55+
// Dedupe based on both path and query if available. This is important
56+
// in Vue CLI so that postcss-loaders with different options can co-exist
57+
: (loader.path + loader.query)
58+
if (!seen.has(identifier)) {
59+
seen.set(identifier, true)
60+
return true
61+
}
62+
})
63+
12564
// Inject style-post-loader before css-loader for scoped CSS and trimming
12665
if (query.type === `style`) {
12766
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
12867
if (cssLoaderIndex > -1) {
12968
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
13069
const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
131-
const request = genRequest([
70+
return genProxyModule([
13271
...afterLoaders,
13372
stylePostLoaderPath,
13473
...beforeLoaders
135-
])
136-
// console.log(request)
137-
return `import mod from ${request}; export default mod; export * from ${request}`
74+
], context)
13875
}
13976
}
14077

141-
// for templates: inject the template compiler & optional cache
142-
if (query.type === `template`) {
143-
const path = require('path')
144-
const cacheLoader = cacheDirectory && cacheIdentifier
145-
? [`${require.resolve('cache-loader')}?${JSON.stringify({
146-
// For some reason, webpack fails to generate consistent hash if we
147-
// use absolute paths here, even though the path is only used in a
148-
// comment. For now we have to ensure cacheDirectory is a relative path.
149-
cacheDirectory: (path.isAbsolute(cacheDirectory)
150-
? path.relative(process.cwd(), cacheDirectory)
151-
: cacheDirectory).replace(/\\/g, '/'),
152-
cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template'
153-
})}`]
154-
: []
155-
156-
const preLoaders = loaders.filter(isPreLoader)
157-
const postLoaders = loaders.filter(isPostLoader)
158-
159-
const request = genRequest([
160-
...cacheLoader,
161-
...postLoaders,
162-
templateLoaderPath + `??vue-loader-options`,
163-
...preLoaders
164-
])
165-
// console.log(request)
166-
return `import mod from ${request}; export default mod;`
167-
}
168-
16978
// if a custom block has no other matching loader other than vue-loader itself
17079
// or cache-loader, we should ignore it
17180
if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
17281
return ``
17382
}
17483

175-
// When the user defines a rule that has only resourceQuery but no test,
176-
// both that rule and the cloned rule will match, resulting in duplicated
177-
// loaders. Therefore it is necessary to perform a dedupe here.
178-
const request = genRequest(loaders)
179-
return `import mod from ${request}; export default mod; export * from ${request}`
84+
// rewrite if we have deduped loaders
85+
if (loaders.length !== rawLoaders.length) {
86+
return genProxyModule(loaders, context)
87+
}
88+
}
89+
90+
function genProxyModule(loaders: Loader[], context: webpack.loader.LoaderContext) {
91+
const loaderStrings = loaders.map(loader => {
92+
return typeof loader === 'string' ? loader : loader.request
93+
})
94+
const resource = context.resourcePath + context.resourceQuery
95+
const request = loaderUtils.stringifyRequest(context, '-!' + [
96+
...loaderStrings,
97+
resource
98+
].join('!'))
99+
// return a proxy module which simply re-exports everything from the
100+
// actual request.
101+
return (
102+
`import mod from ${request};` +
103+
`export default mod;` +
104+
`export * from ${request}`
105+
)
106+
}
107+
108+
function shouldIgnoreCustomBlock(loaders: Loader[]) {
109+
const actualLoaders = loaders.filter(loader => {
110+
// vue-loader
111+
if (loader.path === selfPath) {
112+
return false
113+
}
114+
// cache-loader
115+
if (isCacheLoader(loader)) {
116+
return false
117+
}
118+
return true
119+
})
120+
return actualLoaders.length === 0
180121
}

src/plugin.ts

+43-12
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,7 @@ class VueLoaderPlugin implements webpack.Plugin {
5555
)
5656
}
5757

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

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

71-
// global pitcher (responsible for injecting template compiler loader & CSS
72-
// post loader)
67+
// rule for template compiler
68+
const templateCompilerRule = {
69+
loader: require.resolve('./templateLoader'),
70+
test: /\.vue$/,
71+
resourceQuery: isVueTemplateBlock,
72+
options: vueLoaderOptions
73+
}
74+
75+
// for each rule that matches plain .js files, also create a clone and
76+
// match it against the compiled template code inside *.vue files, so that
77+
// compiled vue render functions receive the same treatment as user code
78+
// (mostly babel)
79+
const matchesJS = createMatcher(`test.js`)
80+
const jsRulesForRenderFn = rules
81+
.filter(r => r !== vueRule && matchesJS(r))
82+
.map(cloneRuleForRenderFn)
83+
84+
// pitcher for block requests (for injecting stylePostLoader and deduping
85+
// loaders matched for src imports)
7386
const pitcher = {
7487
loader: require.resolve('./pitcher'),
7588
resourceQuery: (query: string) => {
7689
const parsed = qs.parse(query.slice(1))
7790
return parsed.vue != null
78-
},
79-
options: {
80-
cacheDirectory: vueLoaderOptions.cacheDirectory,
81-
cacheIdentifier: vueLoaderOptions.cacheIdentifier
8291
}
8392
}
8493

8594
// replace original rules
8695
compiler.options.module!.rules = [
8796
pitcher,
97+
...jsRulesForRenderFn,
98+
templateCompilerRule,
8899
...clonedRules,
89100
...rules
90101
]
@@ -114,7 +125,8 @@ function cloneRule (rule: webpack.RuleSetRule) {
114125
// it in `resourceQuery`. This ensures when we use the normalized rule's
115126
// resource check, include/exclude are matched correctly.
116127
let currentResource: string
117-
const res = Object.assign({}, rule, {
128+
const res = {
129+
...rule,
118130
resource: {
119131
test: (resource: string) => {
120132
currentResource = resource
@@ -138,7 +150,7 @@ function cloneRule (rule: webpack.RuleSetRule) {
138150
}
139151
return true
140152
}
141-
})
153+
}
142154

143155
if (rule.oneOf) {
144156
res.oneOf = rule.oneOf.map(cloneRule)
@@ -147,4 +159,23 @@ function cloneRule (rule: webpack.RuleSetRule) {
147159
return res
148160
}
149161

162+
function isVueTemplateBlock(query: string) {
163+
const parsed = qs.parse(query.slice(1))
164+
return parsed.vue != null && parsed.type === 'template'
165+
}
166+
167+
function cloneRuleForRenderFn(rule: webpack.RuleSetRule) {
168+
const res = {
169+
...rule,
170+
resource: {
171+
test: /\.vue$/
172+
},
173+
resourceQuery: isVueTemplateBlock
174+
}
175+
if (rule.oneOf) {
176+
res.oneOf = rule.oneOf.map(cloneRule)
177+
}
178+
return res
179+
}
180+
150181
module.exports = VueLoaderPlugin

0 commit comments

Comments
 (0)