-
Notifications
You must be signed in to change notification settings - Fork 14
/
normalize.css.chunks.plugin.ts
173 lines (137 loc) · 5.44 KB
/
normalize.css.chunks.plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { compilation, Compiler, Plugin } from 'webpack'
import Chunk = compilation.Chunk;
import Compilation = compilation.Compilation;
import Module = compilation.Module;
import { BabelTarget } from './babel-target'
import { PLUGIN_NAME } from './plugin.name'
// While CSS modules aren't duplicated by targeting the way code modules are, since they are referenced by targeted
// modules, they end up getting duplicated. Without intervention, we'd end up with one CSS file per target, which each
// file containing the exact same content. To fix this, we remove CSS modules from the targeted and move them into their
// own (non-targeted) chunks.
/**
* @internalapi
*/
export class NormalizeCssChunksPlugin implements Plugin {
constructor(private targets: BabelTarget[]) {}
public apply(compiler: Compiler): void {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation: Compilation) => {
if (compilation.name) {
return
}
compilation.hooks.optimizeChunksBasic.tap(PLUGIN_NAME, this.extractCssChunks.bind(this, compilation))
compilation.hooks.optimizeChunkAssets.tap(PLUGIN_NAME, this.cleanCssChunks.bind(this, compilation))
})
}
public extractCssChunks(compilation: Compilation, chunks: Chunk[]): void {
const cssModules: { [name: string]: Set<Module> } = {}
let hasUntaggedTarget = false
// first, find the CSS modules and remove them from their targeted chunks
chunks.forEach(chunk => {
// if `isGeneratedForBabelTargets` is present, we've already processed this chunk
// the `optimizeChunksBasic` hook can get called more than once
if ((chunk as any).isGeneratedForBabelTargets) {
return
}
const target = BabelTarget.findTarget(chunk)
if (!target) {
// can probably skip these? maybe?
}
// don't mess with a chunk if it's not tagged with the target key
if (target && !target.tagAssetsWithKey) {
hasUntaggedTarget = true
return
}
// get the original (untagged) name of the entry module so we can correctly
// attribute any contained CSS modules to the entry
const name = this.findEntryName(chunk)
// track the original entry names to use later
if (!cssModules[name]) {
cssModules[name] = new Set<Module>()
}
chunk.modulesIterable.forEach(module => {
if (module.constructor.name !== 'CssModule') {
return
}
chunk.removeModule(module)
// don't duplicate modules - we should only have one per imported/required CSS/SCSS/etc file
cssModules[name].add(module)
})
})
if (hasUntaggedTarget) {
// untagged targets keep their CSS modules, so we don't need to create a fake one below
return
}
// create chunks for the extracted modules
Object.keys(cssModules).forEach(name => {
const modules = cssModules[name]
const cssGroup = compilation.addChunkInGroup(name)
const cssChunk = cssGroup.chunks[cssGroup.chunks.length - 1]
// HACK ALERT! fool HtmlWebpackPlugin into thinking this is an actual Entrypoint chunk so it
// will include its assets by default (assuming the user hasn't filtered the name of the chunk)
// somewhat relevant: BabelMultiTargetHtmlUpdater.mapChunkNames
cssGroup.isInitial = () => true
cssChunk.hasRuntime = () => false
cssChunk.isInitial = () => true;
(cssChunk as any).isGeneratedForBabelTargets = true
modules.forEach(module => cssChunk.addModule(module))
})
}
private findEntryName(chunk: Chunk): string {
const entry = this.findEntryModule(chunk)
if (entry) {
return entry.reasons[0].dependency.originalName
}
throw new Error(`Could not determine entry module for chunk ${chunk.name}`)
}
private findEntryModule(chunk: Chunk): Module {
if (chunk.entryModule) {
return chunk.entryModule
}
// sure, fine, make me work for it...
for (const group of chunk.groupsIterable) {
for (const groupParent of group.parentsIterable) {
for (const chunk of groupParent.chunks) {
if (chunk.hasEntryModule()) {
return chunk.entryModule
}
}
}
}
// sure, fine, make me REALLY work for it...
for (const module of chunk.modulesIterable) {
const entry = this.getEntryFromModule(module)
if (entry) {
return entry
}
}
}
private getEntryFromModule(module: Module): any {
for (const reason of module.reasons) {
const babelTarget = reason.dependency.babelTarget || BabelTarget.getTargetFromTag(reason.dependency.request, this.targets)
if (babelTarget) {
return module
}
const depEntry = this.getEntryFromModule(reason.dependency.originModule || reason.dependency.module)
if (depEntry) {
return depEntry
}
}
}
// The extract process in extractCssChunks causes a small JavaScript loader file to get generated. Since the file
// gets loaded by HtmlWebpackPlugin, we don't want this file cluttering up the assets, so it gets removed.
public cleanCssChunks(compilation: Compilation, chunks: Chunk[]): void {
chunks.forEach(chunk => {
if (!(chunk as any).isGeneratedForBabelTargets) {
return
}
chunk.files = chunk.files.reduce((result, file) => {
if (file.endsWith('.js')) {
delete compilation.assets[file]
} else {
result.push(file)
}
return result
}, [])
})
}
}