├── .github
└── workflows
│ ├── ci.yml
│ └── release-please.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── package-lock.json
├── package.json
├── src
├── define.ts
├── index.ts
└── lib
│ ├── doesChunkBelongToHtml.ts
│ ├── doesHtmlBeIncluded.ts
│ ├── extractChunks.ts
│ └── generateAttributes.ts
├── tsconfig.json
└── webpack.config.js
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | on: [push, pull_request]
3 | jobs:
4 | runTSCheck:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v3
8 | - uses: actions/setup-node@v3
9 | with:
10 | node-version: '16'
11 | - run: npm install
12 | - name: Typescript check
13 | run: npm run ci
14 | runBuild:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | - uses: actions/setup-node@v3
19 | with:
20 | node-version: '16'
21 | - run: npm install
22 | - name: Build resource
23 | run: npm run build
--------------------------------------------------------------------------------
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - master
5 | pull_request:
6 | branches:
7 | - master
8 | name: release-please
9 | jobs:
10 | release-please:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: google-github-actions/release-please-action@v3
14 | with:
15 | release-type: node
16 | package-name: resource-hint-webpack-plugin
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | types
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx --no -- commitlint --edit ${1}
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run ci --noEmit
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [1.2.1](https://github.com/Ruimve/resource-hint-webpack-plugin/compare/v1.2.0...v1.2.1) (2023-03-12)
4 |
5 |
6 | ### Miscellaneous Chores
7 |
8 | * release 1.2.1 ([435e5fa](https://github.com/Ruimve/resource-hint-webpack-plugin/commit/435e5fad545560640ef796e3e40b107d20b2a861))
9 |
10 | ## [1.2.0](https://github.com/Ruimve/resource-hint-webpack-plugin/compare/v1.1.1...v1.2.0) (2023-03-05)
11 |
12 |
13 | ### Miscellaneous Chores
14 |
15 | * release 1.2.0 ([3400a97](https://github.com/Ruimve/resource-hint-webpack-plugin/commit/3400a972a943991848a60728903b8cbde5dca223))
16 |
17 | ## [1.1.1](https://github.com/Ruimve/resource-hint-webpack-plugin/compare/v1.1.0...v1.1.1) (2023-01-26)
18 |
19 |
20 | ### Miscellaneous Chores
21 |
22 | * release 1.1.1 ([6a22b22](https://github.com/Ruimve/resource-hint-webpack-plugin/commit/6a22b2297910b6b4737c7d9e9391887cdbf8a18a))
23 |
24 | ## [1.1.0](https://github.com/Ruimve/resource-hint-webpack-plugin/compare/v1.0.5...v1.1.0) (2023-01-26)
25 |
26 |
27 | ### Features
28 |
29 | * support dns-prefetch, prerender and preconnect ([047c6a1](https://github.com/Ruimve/resource-hint-webpack-plugin/commit/047c6a19dcf0f9ecc9e4ea676f0d8bb3437be284))
30 |
31 | ## [1.0.5](https://github.com/Ruimve/resource-hint-webpack-plugin/compare/v1.0.4...v1.0.5) (2023-01-22)
32 |
33 |
34 | ### Miscellaneous Chores
35 |
36 | * release 1.0.5 ([3a02ce4](https://github.com/Ruimve/resource-hint-webpack-plugin/commit/3a02ce4650139828f6fa8402f4b5159e0eae4e2b))
37 |
38 | ## [1.0.4](https://github.com/Ruimve/resource-hint-webpack-plugin/compare/v1.0.3...v1.0.4) (2023-01-22)
39 |
40 |
41 | ### Bug Fixes
42 |
43 | * interface format ([5449350](https://github.com/Ruimve/resource-hint-webpack-plugin/commit/54493503ec820f7e6023a5d33899633aca1c3ee3))
44 |
45 | ## [1.0.3](https://github.com/Ruimve/resource-hint-webpack-plugin/compare/v1.0.2...v1.0.3) (2023-01-22)
46 |
47 |
48 | ### Performance Improvements
49 |
50 | * commitlint ([c0867aa](https://github.com/Ruimve/resource-hint-webpack-plugin/commit/c0867aa6942330e1df30189d8a197b0ce9803420))
51 | * prs welcome ([eef3240](https://github.com/Ruimve/resource-hint-webpack-plugin/commit/eef32404e0c616d3211977fc4dc94911af2076de))
52 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for being willing to contribute!
4 |
5 | ## How should I write my commits?
6 |
7 | Please assumes you are using [Conventional Commit messages][conventional-commit-message].
8 |
9 | The most important prefixes you should have in mind are:
10 | ```
11 | fix: which represents bug fixes, and correlates to a SemVer patch.
12 | feat: which represents a new feature, and correlates to a SemVer minor.
13 | feat!:, or fix!:, refactor!:, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major.
14 | ```
15 |
16 |
17 |
18 | [conventional-commit-message]: https://www.conventionalcommits.org
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Ruimve
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
resource-hint-webpack-plugin
3 |
4 |
5 |
10 |
11 |
12 |
快速配置 Resource Hints 的 Webpack 插件
13 |
14 |
15 |
16 | [![Build Status][build-badge]][build]
17 | [![version][version-badge]][package]
18 | [![downloads][downloads-badge]][npmtrends]
19 | [![MIT License][license-badge]][license]
20 | [![PRs Welcome][prs-badge]][prs]
21 |
22 | [![Watch on GitHub][github-watch-badge]][github-watch]
23 | [![Star on GitHub][github-star-badge]][github-star]
24 |
25 | ## 简介
26 |
27 | `resource-hint-webpack-plugin` 集成了 [Resource Hints][resource-hints] 的能力,能够在打包时自动添加 `link` 标签到 `html` 中。
28 |
29 | 基于 [@vuejs/preload-webpack-plugin][v-pwp],强化了配置功能 `options`,并且新增支持 `dns-prefetch / prerender / preconnect` 的能力。
30 |
31 | ```html
32 |
33 |
34 |
35 |
36 |
37 |
38 | ```
39 |
40 | ## 内容列表
41 |
42 | - [预检查](#预检查)
43 | - [安装](#安装)
44 | - [配置项](#配置项)
45 | - [Hints](#hints)
46 | - [preload](#preload)
47 | - [prefetch](#prefetch)
48 | - [dns-prefetch](#dns-prefetch) (新)
49 | - [prerender](#prerender) (新)
50 | - [preconnect](#preconnect) (新)
51 | - [进阶用法](#进阶用法)
52 | - [指定 chunk 和 entry](#指定-chunk-和-entry)
53 | - [指定 htmls](#指定-htmls)
54 | - [批量添加](#批量添加)
55 |
56 | ## 预检查
57 |
58 | 确保 webpack 的版本在 5 以上,并且正在使用 [html-webpack-plugin][hwp]。
59 |
60 | ```ts
61 | const HtmlWebpackPlugin = require('html-webpack-plugin');
62 | const { ResourceHintWebpackPlugin } = require('resource-hint-webpack-plugin');
63 |
64 | module.exports = {
65 | /* ... */
66 | plugins: [
67 | new HtmlWebpackPlugin({
68 | template: './public/index.html',
69 | filename: 'index.html',
70 | chunks: ['index'],
71 | inject: 'body'
72 | }),
73 | new ResourceHintWebpackPlugin([{
74 | rel: 'preload',
75 | include: {
76 | type: 'asyncChunks',
77 | }
78 | }])
79 | ]
80 | /* ... */
81 | }
82 | ```
83 |
84 | ## 安装
85 |
86 | 通过 [npm][npm] 安装,并将其添加到开发时依赖中 `devDependencies`:
87 | ```
88 | npm install resource-hint-webpack-plugin --save-dev
89 | ```
90 | 或者
91 |
92 | 通过 [yarn][yarn] 安装:
93 | ```
94 | yarn add resource-hint-webpack-plugin --dev
95 | ```
96 |
97 | ## 配置项
98 |
99 | 配置项改造成了一个数组,支持传入多个 `options`,下面是单个的配置项:
100 |
101 | |字段名|类型|默认值|描述|
102 | |:---:|:-:|:---:|:--|
103 | |**`rel`**|`{String}`|`-`|脚本的预加载模式|
104 | |**`include`**|`{{IncludeOption}}`|`-`|指定需要预加载的脚本|
105 |
106 | ## Hints
107 |
108 | ### preload
109 |
110 | `preload` 允许预加载在 CSS 和 JavaScript 中定义的资源,并允许决定何时应用每个资源,需要配合 [webpack 懒加载][webpack-lazy]。
111 |
112 | ```ts
113 | new ResourceHintWebpackPlugin([{
114 | rel: 'preload',
115 | include: {
116 | type: 'asyncChunks',
117 | }
118 | }])
119 | ```
120 |
121 | ### prefetch
122 |
123 | `prefetch` 是一个低优先级的资源提示,允许浏览器在后台(空闲时)获取将来可能用得到的资源,并且将他们存储在浏览器的缓存中。
124 |
125 | ```ts
126 | new ResourceHintWebpackPlugin([{
127 | rel: 'prefetch',
128 | include: {
129 | type: 'asyncChunks'
130 | }
131 | }])
132 | ```
133 |
134 | ## dns-prefetch
135 |
136 | `dns-prefetch` 允许浏览器在用户浏览页面时在后台运行 `DNS` 的解析。
137 |
138 | ```ts
139 | new ResourceHintWebpackPlugin([{
140 | rel: 'dns-prefetch',
141 | include: {
142 | hosts: ['//fonts.googleapis.com']
143 | }
144 | }])
145 | ```
146 |
147 | ## prerender
148 |
149 | `prerender` 优化了可能导航到的下一页上的资源的加载,在后台渲染了整个页面和整个页面所有的资源。
150 |
151 | > 要小心的使用 prerender,因为它将会加载很多资源并且可能造成带宽的浪费,尤其是在移动设备上,并且可能会造成一些[副作用][side-effect]。
152 |
153 | ```ts
154 | new ResourceHintWebpackPlugin([{
155 | rel: 'prerender',
156 | include: {
157 | hosts: ['https://www.keycdn.com']
158 | }
159 | }])
160 | ```
161 |
162 | ## preconnect
163 |
164 | `preconnect` 允许浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 `DNS` 解析,`TLS` 协商,`TCP` 握手,这消除了往返延迟并为用户节省了时间。
165 |
166 | ```ts
167 | new ResourceHintWebpackPlugin([{
168 | rel: 'preconnect',
169 | include: {
170 | hosts: ['https://cdn.domain.com']
171 | }
172 | }])
173 | ```
174 |
175 | ## 进阶用法
176 |
177 | ### 指定 chunk 和 entry
178 |
179 | 在使用 `prefetch` 和 `preload` 时,可以指定 `chunks` 或者 `entries` 的值来确定需要生成 `link` 的页面。
180 |
181 | ```ts
182 | module.exports = {
183 | output: {
184 | filename: '[name].[contenthash:8].js',
185 | path: path.resolve(__dirname, 'dist')
186 | }
187 | plugins: [
188 | //...
189 | new ResourceHintWebpackPlugin([{
190 | rel: 'preload', // 或者 prefetch
191 | include: {
192 | chunks: ['index']
193 | }
194 | }])
195 | ]
196 | }
197 | ```
198 |
199 | ```ts
200 | module.exports = {
201 | entry: {
202 | index: './src/index.js',
203 | index2: './src/index2.js'
204 | },
205 | plugins: [
206 | //...
207 | new ResourceHintWebpackPlugin([{
208 | rel: 'preload', // 或者 prefetch
209 | include: {
210 | entries: ['index2']
211 | }
212 | }])
213 | ]
214 | }
215 | ```
216 |
217 | ### 指定 htmls
218 |
219 | 所有的 `hints` 支持指定 `htmls`。
220 |
221 | ```ts
222 | new ResourceHintWebpackPlugin([{
223 | rel: 'dns-prefetch', // prerender, preconnect, preload, prefetch
224 | include: {
225 | htmls: ['index.html']
226 | }
227 | }])
228 | ```
229 |
230 | ### 批量添加
231 |
232 | 本插件增强了 `options` 的能力,能够同时插入不同的 `hints`。
233 |
234 | ```ts
235 | new ResourceHintWebpackPlugin(
236 | [
237 | {
238 | rel: 'preload',
239 | include: {
240 | type: 'asyncChunks'
241 | }
242 | },
243 | {
244 | rel: 'dns-prefetch',
245 | include: {
246 | hosts: ['//fonts.googleapis.com']
247 | }
248 | },
249 | {
250 | rel: 'prerender',
251 | include: {
252 | hosts: ['https://www.keycdn.com']
253 | }
254 | },
255 | {
256 | rel: 'preconnect',
257 | include: {
258 | hosts: ['https://cdn.domain.com']
259 | }
260 | }
261 | ]
262 | )
263 | ```
264 |
265 |
266 |
267 | [npm]: https://www.npmjs.com/
268 | [yarn]: https://classic.yarnpkg.com
269 | [node]: https://nodejs.org
270 | [build-badge]:https://img.shields.io/github/workflow/status/resource-hint-webpack-plugin/validate?logo=github&style=flat-square
271 | [build]: https://github.com/Ruimve/resource-hint-webpack-plugin/actions/workflows/ci.yml/badge.svg
272 | [version-badge]: https://img.shields.io/npm/v/resource-hint-webpack-plugin.svg?style=flat-square
273 | [package]: https://www.npmjs.com/package/resource-hint-webpack-plugin
274 | [downloads-badge]: https://img.shields.io/npm/dm/resource-hint-webpack-plugin.svg?style=flat-square
275 | [npmtrends]: http://www.npmtrends.com/resource-hint-webpack-plugin
276 | [license-badge]: https://img.shields.io/npm/l/resource-hint-webpack-plugin.svg?style=flat-square
277 | [license]: https://github.com/Ruimve/resource-hint-webpack-plugin/blob/master/LICENSE
278 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
279 | [prs]: http://makeapullrequest.com
280 | [github-watch-badge]: https://img.shields.io/github/watchers/Ruimve/resource-hint-webpack-plugin.svg?style=social
281 | [github-watch]: https://github.com/Ruimve/resource-hint-webpack-plugin/watchers
282 | [github-star-badge]: https://img.shields.io/github/stars/Ruimve/resource-hint-webpack-plugin.svg?style=social
283 | [github-star]: https://github.com/Ruimve/resource-hint-webpack-plugin/stargazers
284 |
285 | [resource-hints]:https://www.keycdn.com/blog/resource-hints
286 | [v-pwp]:https://github.com/vuejs/preload-webpack-plugin
287 | [hwp]:https://github.com/ampedandwired/html-webpack-plugin
288 | [webpack-lazy]:https://www.webpackjs.com/guides/lazy-loading/
289 | [side-effect]:https://en.wikipedia.org/wiki/Link_prefetching#Issues_and_criticisms
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | rules: {
4 | 'type-enum': [
5 | 2,
6 | 'always',
7 | [
8 | 'build',
9 | 'chore',
10 | 'ci',
11 | 'docs',
12 | 'feat',
13 | 'fix',
14 | 'perf',
15 | 'refactor',
16 | 'revert',
17 | 'style',
18 | 'test'
19 | ]
20 | ]
21 | }
22 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "resource-hint-webpack-plugin",
3 | "version": "1.2.1",
4 | "description": "webpack 脚本加载插件",
5 | "main": "dist/index.js",
6 | "types": "types/index.d.ts",
7 | "author": "jingyu",
8 | "license": "MIT",
9 | "scripts": {
10 | "dev": "webpack --mode development",
11 | "build": "webpack",
12 | "ci": "tsc --noEmit",
13 | "prepare": "husky install"
14 | },
15 | "files": [
16 | "dist",
17 | "types"
18 | ],
19 | "keywords": [
20 | "javascript",
21 | "webpack",
22 | "plugin",
23 | "script",
24 | "prefetch",
25 | "preload",
26 | "link"
27 | ],
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/Ruimve/resource-hint-webpack-plugin.git"
31 | },
32 | "bugs": {
33 | "url": "https://github.com/Ruimve/resource-hint-webpack-plugin/issues"
34 | },
35 | "homepage": "https://github.com/Ruimve/resource-hint-webpack-plugin#readme",
36 | "dependencies": {
37 | "html-webpack-plugin": "^5.5.0",
38 | "path-browserify": "^1.0.1"
39 | },
40 | "devDependencies": {
41 | "@commitlint/cli": "^17.4.2",
42 | "@commitlint/config-conventional": "^17.4.2",
43 | "husky": "^8.0.3",
44 | "ts-loader": "^9.4.2",
45 | "typescript": "^4.9.4",
46 | "webpack": "^5.75.0",
47 | "webpack-cli": "^5.0.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/define.ts:
--------------------------------------------------------------------------------
1 | import HtmlWebpackPlugin from "html-webpack-plugin";
2 |
3 | export enum RelType {
4 | preload = 'preload',
5 | prefetch = 'prefetch',
6 | dnsPrefetch = 'dns-prefetch',
7 | prerender = 'prerender',
8 | preconnect = 'preconnect'
9 | }
10 |
11 | export enum IncludeType {
12 | initial = 'initial',
13 | allChunks = 'allChunks',
14 | allAssets = 'allAssets',
15 | asyncChunks = 'asyncChunks'
16 | }
17 |
18 | export interface IncludeOption {
19 | type?: IncludeType;
20 | hosts?: string[];
21 | chunks?: string[];
22 | entries?: string[];
23 | htmls?: string[];
24 | }
25 |
26 | export interface ResourceHintOption {
27 | rel: RelType;
28 | include?: IncludeOption;
29 | }
30 |
31 | export interface HtmlPluginData {
32 | assets: {
33 | publicPath: string;
34 | js: string[];
35 | css: string[];
36 | favicon?: string | undefined;
37 | manifest?: string | undefined;
38 | };
39 | outputName: string;
40 | plugin: HtmlWebpackPlugin;
41 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { ResourceHintOption, HtmlPluginData, RelType, IncludeType } from './define';
2 | import { Compiler, Compilation } from "webpack";
3 |
4 | import HtmlWebpackPlugin, { HtmlTagObject } from "html-webpack-plugin";
5 |
6 | import { extractChunks } from './lib/extractChunks';
7 | import { doesChunkBelongToHtml } from './lib/doesChunkBelongToHtml';
8 | import { generateAttributes } from './lib/generateAttributes';
9 | import { doesHtmlBeIncluded } from './lib/doesHtmlBeIncluded';
10 |
11 | export class ResourceHintWebpackPlugin {
12 | options: ResourceHintOption[];
13 | resourceHints: HtmlTagObject[];
14 | constructor(options: ResourceHintOption[]) {
15 | /** 聚合默认选项和用户选项 */
16 | this.options = options || [];
17 | this.resourceHints = [];
18 | }
19 |
20 | generateCommonLinks(options: ResourceHintOption, compilation: Compilation, htmlPluginData: HtmlPluginData): HtmlTagObject[] {
21 | /** 获取选项中的 include 参数 */
22 | const { include = { type: IncludeType.asyncChunks } } = options;
23 | /** 根据用户选项筛选需要的 chunk */
24 | const extractedChunks = extractChunks(compilation, include);
25 | /** 匹配当前 html 的 chunk */
26 | const htmlChunks = include.type === IncludeType.allAssets ? extractedChunks : extractedChunks.filter(
27 | chunk => doesChunkBelongToHtml(chunk, htmlPluginData.plugin.options)
28 | );
29 | /** 获取所有 chunk 中的所有文件 */
30 | const allFiles = htmlChunks.reduce((accumulated, chunk) => accumulated.concat(Array.from(chunk.files)), []);
31 | /** 除去重复的文件 */
32 | const uniqueFiles = Array.from(new Set(allFiles));
33 | /** 对文件进行排序 */
34 | const sortedFiles = uniqueFiles.sort();
35 | /** 获取配置的 CDN 路径 */
36 | const webpackPublicPath = compilation.outputOptions.publicPath;
37 | const publicPath = webpackPublicPath && webpackPublicPath !== 'auto' ? webpackPublicPath : '';
38 | /** prefetch and preload */
39 | const commonLinks: HtmlTagObject[] = sortedFiles.map(file => {
40 | const href = `${publicPath}${file}`;
41 | const attributes = generateAttributes(href, options.rel);
42 | return {
43 | tagName: 'link',
44 | voidTag: false,
45 | meta: {},
46 | attributes
47 | }
48 | });
49 | return commonLinks;
50 | }
51 |
52 | generateDNSLinks(options: ResourceHintOption, compilation: Compilation, htmlPluginData: HtmlPluginData): HtmlTagObject[] {
53 | const include = options.include;
54 | /** dns-prefetch */
55 | const dnsLinks = (include?.hosts || []).map(d => {
56 | const attributes = {
57 | rel: RelType.dnsPrefetch,
58 | href: d
59 | }
60 | return {
61 | tagName: 'link',
62 | voidTag: false,
63 | meta: {},
64 | attributes
65 | }
66 | });
67 | return dnsLinks;
68 | }
69 |
70 | generateRenderLinks(options: ResourceHintOption, compilation: Compilation, htmlPluginData: HtmlPluginData): HtmlTagObject[] {
71 | const include = options.include;
72 | /** prerender */
73 | const renderLink = (include?.hosts || []).map(r => {
74 | const attributes = {
75 | rel: RelType.prerender,
76 | href: r
77 | }
78 | return {
79 | tagName: 'link',
80 | voidTag: false,
81 | meta: {},
82 | attributes
83 | }
84 | });
85 | return renderLink;
86 | }
87 |
88 | generateConnectLinks(options: ResourceHintOption, compilation: Compilation, htmlPluginData: HtmlPluginData): HtmlTagObject[] {
89 | const include = options.include;
90 | /** preconnect */
91 | const connectLink = (include?.hosts || []).map(c => {
92 | const attributes = {
93 | rel: RelType.preconnect,
94 | href: c,
95 | crossorigin: ''
96 | }
97 | return {
98 | tagName: 'link',
99 | voidTag: false,
100 | meta: {},
101 | attributes
102 | }
103 | });
104 | return connectLink;
105 | }
106 |
107 | generateLinks(compilation: Compilation, htmlPluginData: HtmlPluginData): void {
108 | const optionsArray = this.options;
109 | const links = optionsArray.reduce((pre, options) => {
110 | /** 配置需要插入的 html */
111 | if (!doesHtmlBeIncluded(htmlPluginData.outputName, options.include?.htmls)) {
112 | return pre.concat([]);
113 | }
114 |
115 | if ([RelType.preload, RelType.prefetch].includes(options.rel)) {
116 | const commonLinks = this.generateCommonLinks(options, compilation, htmlPluginData);
117 | return pre.concat(commonLinks);
118 | }
119 |
120 | if ([RelType.dnsPrefetch].includes(options.rel)) {
121 | const dnsLinks = this.generateDNSLinks(options, compilation, htmlPluginData);
122 | return pre.concat(dnsLinks);
123 | }
124 |
125 | if ([RelType.prerender].includes(options.rel)) {
126 | const renderLinks = this.generateRenderLinks(options, compilation, htmlPluginData);
127 | return pre.concat(renderLinks);
128 | }
129 |
130 | if ([RelType.preconnect].includes(options.rel)) {
131 | const connectLinks = this.generateConnectLinks(options, compilation, htmlPluginData);
132 | return pre.concat(connectLinks);
133 | }
134 |
135 | return pre.concat([]);
136 | }, []);
137 |
138 | this.resourceHints = links;
139 | }
140 |
141 | apply(compiler: Compiler): void {
142 | compiler.hooks.compilation.tap(
143 | 'ResourceHintWebpackPlugin',
144 | (compilation: Compilation) => {
145 | HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
146 | 'ResourceHintWebpackPlugin',
147 | (htmlPluginData, callback) => {
148 | this.generateLinks(compilation, htmlPluginData);
149 | callback();
150 | }
151 | );
152 |
153 | HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
154 | 'ResourceHintWebpackPlugin',
155 | (htmlPluginData, callback) => {
156 | htmlPluginData.assetTags.styles = [
157 | ...this.resourceHints,
158 | ...htmlPluginData.assetTags.styles
159 | ];
160 | callback();
161 | }
162 | );
163 | }
164 | )
165 | }
166 | }
--------------------------------------------------------------------------------
/src/lib/doesChunkBelongToHtml.ts:
--------------------------------------------------------------------------------
1 | import HtmlWebpackPlugin from "html-webpack-plugin";
2 | import { Chunk } from 'webpack';
3 |
4 | function doesChunkBelongToHtml(
5 | chunk: Chunk,
6 | htmlPluginOptions: HtmlWebpackPlugin.ProcessedOptions | undefined
7 | ) {
8 | const chunkName = recursiveChunkEntryName(chunk);
9 | return isChunksFiltered(chunkName, htmlPluginOptions?.chunks, htmlPluginOptions?.excludeChunks);
10 | }
11 |
12 | function recursiveChunkEntryName (chunk: Chunk) {
13 | const [chunkGroup] = chunk.groupsIterable
14 | return _recursiveChunkGroup(chunkGroup)
15 | }
16 |
17 | function _recursiveChunkGroup (chunkGroup) {
18 | if (chunkGroup.constructor.name === 'Entrypoint') {
19 | return chunkGroup.name;
20 | } else {
21 | const [chunkParent] = chunkGroup.getParents();
22 | return _recursiveChunkGroup(chunkParent);
23 | }
24 | }
25 |
26 | function isChunksFiltered(chunkName?: string, includedChunks?: 'all' | string[], excludedChunks?: string[]) {
27 | if (includedChunks === 'all' || typeof includedChunks === undefined) {
28 | return true;
29 | }
30 |
31 | if (Array.isArray(includedChunks) && (typeof chunkName === 'undefined' || !includedChunks.includes(chunkName))) {
32 | return false
33 | }
34 | if (Array.isArray(excludedChunks) && (typeof chunkName === 'undefined' || excludedChunks.includes(chunkName))) {
35 | return false
36 | }
37 | return true
38 | }
39 |
40 | export {
41 | doesChunkBelongToHtml
42 | }
--------------------------------------------------------------------------------
/src/lib/doesHtmlBeIncluded.ts:
--------------------------------------------------------------------------------
1 | function doesHtmlBeIncluded(outputHtml: string, htmlBeIncluded?: string[]) {
2 | if(typeof htmlBeIncluded === 'undefined'){
3 | return true;
4 | }else{
5 | return htmlBeIncluded.includes(outputHtml);
6 | }
7 | }
8 |
9 | export {
10 | doesHtmlBeIncluded
11 | }
--------------------------------------------------------------------------------
/src/lib/extractChunks.ts:
--------------------------------------------------------------------------------
1 | import { IncludeOption, IncludeType } from '../define';
2 | import { Chunk, Compilation } from "webpack";
3 |
4 | function isAsync(chunk: Chunk) {
5 | return !chunk.canBeInitial();
6 | }
7 |
8 | function getChunkEntryName(chunk: Chunk) {
9 | const entryOptions = chunk.getEntryOptions();
10 | return entryOptions?.name;
11 | }
12 |
13 | function extractChunks(compilation: Compilation, include: IncludeOption) {
14 | let chunks = Array.from(compilation.chunks);
15 |
16 | const includeType = include?.type;
17 | const includeChunks = include?.chunks;
18 | const includeEntries = include?.entries;
19 |
20 | if (Array.isArray(includeChunks)) {
21 | chunks = chunks.filter(chunk => chunk.name && includeChunks?.includes(chunk.name));
22 | }
23 |
24 | if (Array.isArray(includeEntries)) {
25 | chunks = chunks.filter(
26 | chunk => {
27 | const name = getChunkEntryName(chunk);
28 | return includeEntries?.includes(name!);
29 | }
30 | );
31 | }
32 |
33 | if (includeType === IncludeType.initial) {
34 | return chunks.filter(chunk => !isAsync(chunk));
35 | }
36 |
37 | if (includeType === IncludeType.asyncChunks) {
38 | return chunks.filter(chunk => isAsync(chunk));
39 | }
40 |
41 | if (includeType === IncludeType.allChunks) {
42 | return chunks;
43 | }
44 |
45 | if (includeType === IncludeType.allAssets) {
46 | return [
47 | {
48 | files: Object.keys(compilation.assets)
49 | }
50 | ] as unknown as Chunk[];
51 | }
52 |
53 | return chunks;
54 | }
55 |
56 | export {
57 | extractChunks
58 | }
--------------------------------------------------------------------------------
/src/lib/generateAttributes.ts:
--------------------------------------------------------------------------------
1 | import { RelType } from '../define';
2 | import path from 'path';
3 |
4 | const fonts = ['.eot', '.otf', '.fon', '.font', '.ttf', '.ttc', '.woff', '.woff2'];
5 |
6 | interface Attribute {
7 | [attributeName: string]: string | boolean | null | undefined;
8 | }
9 | function generateAttributes(href: string, rel: RelType) {
10 | const attributes: Attribute = {
11 | href,
12 | rel
13 | }
14 |
15 | if (rel === RelType.preload) {
16 | const url = new URL(href, 'https://github.com');
17 | const extension = path.extname(url.pathname);
18 |
19 | if (fonts.includes(extension)) {
20 | attributes.as = 'font';
21 | attributes.crossorigin = '';
22 | }
23 |
24 | if (extension === '.css') {
25 | attributes.as = 'style';
26 | }
27 |
28 | if (extension === '.js') {
29 | attributes.as = 'script';
30 | }
31 |
32 | }
33 |
34 | return attributes;
35 | }
36 |
37 | export {
38 | generateAttributes
39 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "CommonJS", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
46 | /* Emit */
47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52 | "outDir": "./types", /* Specify an output folder for all emitted files. */
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 |
78 | /* Type Checking */
79 | "strict": true, /* Enable all strict type-checking options. */
80 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | },
103 | "include": ["src/**/*"],
104 | "exclude": ["node_modules"]
105 | }
106 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = {
5 | mode: 'production',
6 | entry: './src/index.ts',
7 | output: {
8 | filename: 'index.js',
9 | path: path.resolve(__dirname, 'dist'),
10 | library: {
11 | type: 'commonjs'
12 | }
13 | },
14 | devtool: false,
15 | externals: {
16 | 'html-webpack-plugin': 'commonjs html-webpack-plugin',
17 | 'path': 'path-browserify'
18 | },
19 | resolve: {
20 | extensions: ['.js', '.ts'],
21 | fallback: {
22 | "path": require.resolve("path-browserify")
23 | }
24 | },
25 | module: {
26 | rules: [
27 | {
28 | test: /\.ts$/,
29 | use: ['ts-loader']
30 | }
31 | ]
32 | },
33 | plugins: [
34 | new webpack.CleanPlugin()
35 | ]
36 | }
--------------------------------------------------------------------------------