Skip to content

Commit 60f0c15

Browse files
committed
feat: support html-webpack-plugin v4
1 parent 85b9974 commit 60f0c15

File tree

9 files changed

+368
-169
lines changed

9 files changed

+368
-169
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
},
4545
"homepage": "https://github.com/Runjuu/html-inline-css-webpack-plugin#readme",
4646
"devDependencies": {
47+
"@types/html-webpack-plugin": "^3.2.1",
4748
"@types/lodash": "^4.14.137",
4849
"@types/webpack": "^4.4.0",
4950
"husky": "^1.3.1",
@@ -56,6 +57,9 @@
5657
"typescript": "^3.2.2",
5758
"webpack": "^4.11.1"
5859
},
60+
"peerDependencies": {
61+
"html-webpack-plugin": "^3.0.0"
62+
},
5963
"dependencies": {
6064
"lodash": "^4.17.15"
6165
}

src/core/base-plugin.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Config, DEFAULT_REPLACE_CONFIG, FileCache } from '../types'
2+
import { isCSS, escapeRegExp } from '../utils'
3+
4+
interface Asset {
5+
source(): string
6+
size(): number
7+
}
8+
9+
interface Compilation {
10+
assets: { [key: string]: Asset }
11+
}
12+
13+
export class BasePlugin {
14+
protected cssStyleCache: FileCache = {}
15+
16+
protected get replaceConfig() {
17+
return this.config.replace || DEFAULT_REPLACE_CONFIG
18+
}
19+
20+
constructor(protected readonly config: Config = {}) {}
21+
22+
protected prepare({ assets }: Compilation) {
23+
Object.keys(assets).forEach((fileName) => {
24+
if (isCSS(fileName) && this.isCurrentFileNeedsToBeInlined(fileName)) {
25+
this.cssStyleCache[fileName] = assets[fileName].source()
26+
27+
if (!this.config.leaveCSSFile) {
28+
delete assets[fileName]
29+
}
30+
}
31+
})
32+
}
33+
34+
protected getCSSStyle({
35+
cssLink,
36+
publicPath,
37+
}: {
38+
cssLink: string
39+
publicPath: string
40+
}): string | undefined {
41+
// Link pattern: publicPath + fileName + '?' + hash
42+
const fileName = cssLink
43+
.replace(new RegExp(`^${escapeRegExp(publicPath)}`), '')
44+
.replace(/\?.+$/g, '')
45+
46+
if (this.isCurrentFileNeedsToBeInlined(fileName)) {
47+
const style = this.cssStyleCache[fileName]
48+
49+
if (style === undefined) {
50+
console.error(
51+
`Can not get css style for ${cssLink}. It may be a bug of html-inline-css-webpack-plugin.`,
52+
)
53+
}
54+
55+
return style
56+
} else {
57+
return undefined
58+
}
59+
}
60+
61+
protected isCurrentFileNeedsToBeInlined(fileName: string): boolean {
62+
if (typeof this.config.filter === 'function') {
63+
return this.config.filter(fileName)
64+
} else {
65+
return true
66+
}
67+
}
68+
69+
protected addStyle({
70+
html,
71+
htmlFileName,
72+
style,
73+
}: {
74+
html: string
75+
htmlFileName: string
76+
style: string
77+
}) {
78+
const styleString = `<style type="text/css">${style}</style>`
79+
const replaceValues = [styleString, this.replaceConfig.target]
80+
81+
if (this.replaceConfig.position === 'after') {
82+
replaceValues.reverse()
83+
}
84+
85+
if (html.indexOf(this.replaceConfig.target) === -1) {
86+
throw new Error(
87+
`Can not inject css style into "${htmlFileName}", as there is not replace target "${
88+
this.replaceConfig.target
89+
}"`,
90+
)
91+
}
92+
93+
return html.replace(this.replaceConfig.target, replaceValues.join(''))
94+
}
95+
96+
protected cleanUp(html: string) {
97+
return this.replaceConfig.removeTarget
98+
? html.replace(this.replaceConfig.target, '')
99+
: html
100+
}
101+
}

src/core/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './v3'
2+
export * from './v4'

src/core/v3.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { SyncHook } from 'tapable'
2+
import { Compiler } from 'webpack'
3+
4+
import { TAP_KEY_PREFIX } from '../types'
5+
import { BasePlugin } from './base-plugin'
6+
7+
interface HtmlWebpackPluginData {
8+
html: string
9+
outputName: string
10+
assets: {
11+
publicPath: string
12+
css: string[]
13+
}
14+
}
15+
16+
export class PluginForHtmlWebpackPluginV3 extends BasePlugin {
17+
private process(data: HtmlWebpackPluginData) {
18+
// check if current html needs to be inlined
19+
if (this.isCurrentFileNeedsToBeInlined(data.outputName)) {
20+
data.assets.css.forEach((cssLink, index) => {
21+
const style = this.getCSSStyle({
22+
cssLink,
23+
publicPath: data.assets.publicPath,
24+
})
25+
26+
if (style) {
27+
data.html = this.addStyle({
28+
html: data.html,
29+
htmlFileName: data.outputName,
30+
style: style,
31+
})
32+
33+
// prevent generate <link /> tag
34+
data.assets.css.splice(index, 1)
35+
}
36+
})
37+
38+
data.html = this.cleanUp(data.html)
39+
}
40+
}
41+
42+
apply(compiler: Compiler) {
43+
compiler.hooks.compilation.tap(
44+
`${TAP_KEY_PREFIX}_compilation`,
45+
(compilation) => {
46+
if ('htmlWebpackPluginBeforeHtmlProcessing' in compilation.hooks) {
47+
const hook: SyncHook<HtmlWebpackPluginData> =
48+
// @ts-ignore Error:(130, 27) TS2339: Property 'htmlWebpackPluginBeforeHtmlProcessing' does not exist on type 'CompilationHooks'.
49+
compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing
50+
hook.tap(
51+
`${TAP_KEY_PREFIX}_htmlWebpackPluginBeforeHtmlProcessing`,
52+
(data: HtmlWebpackPluginData) => {
53+
this.prepare(compilation)
54+
this.process(data)
55+
},
56+
)
57+
} else {
58+
throw new Error(
59+
'`html-webpack-plugin` should be ordered first before html-inline-css-webpack-plugin',
60+
)
61+
}
62+
},
63+
)
64+
}
65+
}

src/core/v4.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { SyncHook } from 'tapable'
2+
import { Compiler } from 'webpack'
3+
import HTMLWebpackPlugin = require('html-webpack-plugin')
4+
5+
import { TAP_KEY_PREFIX } from '../types'
6+
import { BasePlugin } from './base-plugin'
7+
8+
interface BeforeAssetTagGenerationData {
9+
outputName: string
10+
assets: {
11+
publicPath: string
12+
css: string[]
13+
}
14+
plugin: HTMLWebpackPlugin
15+
}
16+
17+
interface BeforeEmitData {
18+
html: string
19+
outputName: string
20+
plugin: HTMLWebpackPlugin
21+
}
22+
23+
interface HTMLWebpackPluginHooks {
24+
beforeAssetTagGeneration: SyncHook<BeforeAssetTagGenerationData>
25+
beforeEmit: SyncHook<BeforeEmitData>
26+
}
27+
28+
type CSSStyle = string
29+
30+
export class PluginForHtmlWebpackPluginV4 extends BasePlugin {
31+
// Using object reference to distinguish styles for multiple files
32+
private cssStyleMap: Map<HTMLWebpackPlugin, CSSStyle[]> = new Map()
33+
34+
private prepareCSSStyle(data: BeforeAssetTagGenerationData) {
35+
data.assets.css.forEach((cssLink, index) => {
36+
if (this.isCurrentFileNeedsToBeInlined(cssLink)) {
37+
const style = this.getCSSStyle({
38+
cssLink,
39+
publicPath: data.assets.publicPath,
40+
})
41+
42+
if (style) {
43+
if (this.cssStyleMap.has(data.plugin)) {
44+
this.cssStyleMap.get(data.plugin)!.push(style)
45+
} else {
46+
this.cssStyleMap.set(data.plugin, [style])
47+
}
48+
49+
// prevent generate <link /> tag
50+
data.assets.css.splice(index, 1)
51+
}
52+
}
53+
})
54+
}
55+
56+
private process(data: BeforeEmitData) {
57+
// check if current html needs to be inlined
58+
if (this.isCurrentFileNeedsToBeInlined(data.outputName)) {
59+
const cssStyles = this.cssStyleMap.get(data.plugin) || []
60+
61+
cssStyles.forEach((style) => {
62+
data.html = this.addStyle({
63+
style,
64+
html: data.html,
65+
htmlFileName: data.outputName,
66+
})
67+
})
68+
69+
data.html = this.cleanUp(data.html)
70+
}
71+
}
72+
73+
apply(compiler: Compiler) {
74+
compiler.hooks.compilation.tap(
75+
`${TAP_KEY_PREFIX}_compilation`,
76+
(compilation) => {
77+
const hooks: HTMLWebpackPluginHooks = (HTMLWebpackPlugin as any).getHooks(
78+
compilation,
79+
)
80+
81+
hooks.beforeAssetTagGeneration.tap(
82+
`${TAP_KEY_PREFIX}_beforeAssetTagGeneration`,
83+
(data) => {
84+
this.prepare(compilation)
85+
this.prepareCSSStyle(data)
86+
},
87+
)
88+
89+
hooks.beforeEmit.tap(`${TAP_KEY_PREFIX}_beforeEmit`, (data) => {
90+
this.process(data)
91+
})
92+
},
93+
)
94+
}
95+
}

0 commit comments

Comments
 (0)