UNPKG

8.18 kBJavaScriptView Raw
1const loaderUtils = require('loader-utils')
2const path = require('path')
3const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin')
4const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin')
5const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin')
6const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin')
7const LimitChunkCountPlugin = require('webpack/lib/optimize/LimitChunkCountPlugin')
8const normalize = require('./utils/normalize')
9const parseRequest = require('./utils/parse-request')
10const getMainCompilation = require('./utils/get-main-compilation')
11const toPosix = require('./utils/to-posix')
12const config = require('./config')
13const hash = require('hash-sum')
14const fixRelative = require('./utils/fix-relative')
15
16const defaultResultSource = '// removed by extractor'
17
18module.exports = function (content) {
19 this.cacheable()
20 const options = loaderUtils.getOptions(this) || {}
21
22 const mainCompilation = getMainCompilation(this._compilation)
23 const mpx = mainCompilation.__mpx__
24
25 const packageName = mpx.currentPackageRoot || 'main'
26 const pagesMap = mpx.pagesMap
27 const componentsMap = mpx.componentsMap[packageName]
28
29 const extract = mpx.extract
30 const extractedMap = mpx.extractedMap
31 const mode = mpx.mode
32 const seenFile = mpx.extractSeenFile
33 const typeExtMap = config[mode].typeExtMap
34
35 const rootName = mainCompilation._preparedEntrypoints[0].name
36 const rootRequest = mainCompilation._preparedEntrypoints[0].request
37 const rootModule = mainCompilation.entries.find((module) => {
38 return module.rawRequest === rootRequest
39 })
40 const rootResourcePath = parseRequest(rootModule.resource).resourcePath
41
42 const resourceRaw = this.resource
43 const issuerResourceRaw = options.issuerResource
44
45 let resultSource = defaultResultSource
46
47 const getFile = (resourceRaw, type) => {
48 const resourcePath = parseRequest(resourceRaw).resourcePath
49 const id = `${mode}:${packageName}:${type}:${resourcePath}`
50 if (!seenFile[id]) {
51 const resourcePath = parseRequest(resourceRaw).resourcePath
52 let filename = pagesMap[resourcePath] || componentsMap[resourcePath]
53 if (!filename && resourcePath === rootResourcePath) {
54 filename = rootName
55 }
56
57 if (filename) {
58 seenFile[id] = filename + typeExtMap[type]
59 } else {
60 const resourceName = path.parse(resourcePath).name
61 const outputPath = path.join(type, resourceName + hash(resourcePath) + typeExtMap[type])
62 seenFile[id] = mpx.getPackageInfo(resourceRaw, {
63 outputPath,
64 isStatic: true,
65 error: (err) => {
66 this.emitError(err)
67 },
68 warn: (err) => {
69 this.emitWarning(err)
70 }
71 }).outputPath
72 }
73 }
74 return seenFile[id]
75 }
76
77 const type = options.type
78 const fromImport = options.fromImport
79 let index = +options.index
80
81 let issuerFile
82 if (issuerResourceRaw) {
83 issuerFile = getFile(issuerResourceRaw, type)
84 }
85
86 const file = getFile(resourceRaw, type)
87 const filename = /(.*)\..*/.exec(file)[1]
88
89 let sideEffects = () => {
90 }
91
92 if (index === -1) {
93 // 需要返回路径或产生副作用
94 switch (type) {
95 // styles中index为-1就两种情况,一种是.mpx中使用src引用样式,第二种为css-loader中处理@import
96 case 'styles':
97 if (issuerFile) {
98 let relativePath = toPosix(path.relative(path.dirname(issuerFile), file))
99 relativePath = fixRelative(relativePath, mode)
100 if (fromImport) {
101 resultSource = `module.exports = ${JSON.stringify(relativePath)};`
102 } else {
103 const issuer = this._module.issuer
104 // todo 此处只同步issuer模块的文件依赖不能保障完全正确,理想状态下还应该同步issuerFile对应的内联样式模块中的文件依赖,因为内联样式模块还可能通过url或者是@import引入新的文件依赖;
105 // todo 在当前架构设计下src样式模块和内联样式模块无法保障处理顺序,此时可能无法拿到内联样式模块的文件依赖;
106 // todo 当前做法能够保障大多数场景下的正确,先作为临时处理方式,待后续优化。
107 // todo 这种模块间在构建期相互影响的设计模式存在很大不可控制性,后续优化时考虑全面移除该类设计。
108 if (issuer) {
109 issuer.buildInfo.fileDependencies.forEach((dep) => {
110 this.addDependency(dep)
111 })
112 issuer.buildInfo.contextDependencies.forEach((dep) => {
113 this.addContextDependency(dep)
114 })
115 }
116 sideEffects = (additionalAssets) => {
117 additionalAssets[issuerFile] = additionalAssets[issuerFile] || []
118 additionalAssets[issuerFile].prefix = additionalAssets[issuerFile].prefix || []
119 additionalAssets[issuerFile].prefix.push(`@import "${relativePath}";\n`)
120 }
121 }
122 }
123 break
124 case 'template':
125 resultSource = `module.exports = __webpack_public_path__ + ${JSON.stringify(file)};`
126 break
127 }
128 index = 0
129 }
130
131 const id = `${file}:${index}:${issuerFile}:${fromImport}`
132
133 let nativeCallback
134 // 由于webpack中moduleMap只在compilation维度有效,不同子编译之间可能会对相同的引用文件进行重复的无效抽取,建立全局extractedMap避免这种情况出现
135 if (extractedMap[id]) {
136 return extractedMap[id]
137 } else {
138 extractedMap[id] = resultSource
139 nativeCallback = this.async()
140 }
141
142 // 使用子编译器生成需要抽离的json,styles和template
143 const contentLoader = normalize.lib('content-loader')
144 const request = `!!${contentLoader}?${JSON.stringify(options)}!${this.resource}`
145
146 const childFilename = 'extractor-filename'
147 const outputOptions = {
148 filename: childFilename
149 }
150 const childCompiler = mainCompilation.createChildCompiler(request, outputOptions, [
151 new NodeTemplatePlugin(outputOptions),
152 new LibraryTemplatePlugin(null, 'commonjs2'),
153 new NodeTargetPlugin(),
154 new SingleEntryPlugin(this.context, request, filename),
155 new LimitChunkCountPlugin({ maxChunks: 1 })
156 ])
157
158 childCompiler.hooks.thisCompilation.tap('MpxWebpackPlugin ', (compilation) => {
159 compilation.hooks.normalModuleLoader.tap('MpxWebpackPlugin', (loaderContext) => {
160 // 传递编译结果,子编译器进入content-loader后直接输出
161 loaderContext.__mpx__ = {
162 content,
163 fileDependencies: this.getDependencies(),
164 contextDependencies: this.getContextDependencies()
165 }
166 })
167 })
168
169 let source
170
171 childCompiler.hooks.afterCompile.tapAsync('MpxWebpackPlugin', (compilation, callback) => {
172 source = compilation.assets[childFilename] && compilation.assets[childFilename].source()
173
174 // Remove all chunk assets
175 compilation.chunks.forEach((chunk) => {
176 chunk.files.forEach((file) => {
177 delete compilation.assets[file]
178 })
179 })
180
181 callback()
182 })
183
184 childCompiler.runAsChild((err, entries, compilation) => {
185 if (err) return nativeCallback(err)
186 if (compilation.errors.length > 0) {
187 mainCompilation.errors.push(...compilation.errors)
188 }
189
190 compilation.fileDependencies.forEach((dep) => {
191 this.addDependency(dep)
192 }, this)
193 compilation.contextDependencies.forEach((dep) => {
194 this.addContextDependency(dep)
195 }, this)
196
197 if (!source) {
198 return nativeCallback(new Error('Didn\'t get a result from child compiler'))
199 }
200
201 try {
202 let text = this.exec(source, request)
203 if (Array.isArray(text)) {
204 text = text.map((item) => {
205 return item[1]
206 }).join('\n')
207 }
208
209 extract(text, file, index, sideEffects)
210
211 // 在production模式下移除extract残留空模块
212 if (resultSource === defaultResultSource && this.minimize) {
213 this._module.needRemove = true
214 }
215 } catch (err) {
216 return nativeCallback(err)
217 }
218 if (resultSource) {
219 nativeCallback(null, resultSource)
220 } else {
221 nativeCallback()
222 }
223 })
224}