Skip to content

Commit c789e39

Browse files
committed
feat: add the filter for the preload option
1 parent c0e30a8 commit c789e39

25 files changed

Lines changed: 393 additions & 58 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 4.14.0 (2025-01-19)
4+
5+
- feat: add the `filter` for the `preload` option
6+
37
## 4.13.0 (2025-01-18)
48

59
- feat: add support for preloading of dynamic imported modules, #138

README.md

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,16 +1660,16 @@ new HtmlBundlerPlugin({
16601660
16611661
### `entryFilter`
16621662
1663-
Filter to process only matching template files.
1663+
The filter to process only matching template files.
16641664
This option works only if the [entry](#option-entry-path) option is a path.
16651665
16661666
Type:
16671667
```ts
1668-
type entryFilter =
1668+
type AdvancedFilter =
16691669
| RegExp
16701670
| Array<RegExp>
16711671
| { includes?: Array<RegExp>; excludes?: Array<RegExp> }
1672-
| ((file: string) => void | false);
1672+
| ((file: string) => void | true | false);
16731673
```
16741674
16751675
Default value:
@@ -2501,20 +2501,30 @@ Type:
25012501
```ts
25022502
type Preload = Array<{
25032503
test: RegExp;
2504+
filter?: AdvancedFilter;
25042505
as?: string;
25052506
rel?: string;
25062507
type?: string;
25072508
attributes?: { [attributeName: string]: string | boolean };
25082509
}>;
25092510
```
25102511
2512+
```ts
2513+
type AdvancedFilter =
2514+
| RegExp
2515+
| Array<RegExp>
2516+
| { includes?: Array<RegExp>; excludes?: Array<RegExp> }
2517+
| ((value: string) => void | true | false);
2518+
```
2519+
25112520
Default: `null`
25122521
25132522
Generates and injects preload tags `<link rel="preload">` in the head before all `link` or `script` tags for all matching source assets resolved in templates and styles.
25142523
25152524
The descriptions of the properties:
25162525
25172526
- `test` - an RegEpx to match source asset files.
2527+
- `filter` - an advanced filter to preload only matched output asset files.
25182528
- `as` - a content type, one of `audio` `document` `embed` `font` `image` `object` `script` `style` `track` `video` `worker`
25192529
- `rel` - a value indicates how to load a resource, one of `preload` `prefetch` , defaults `preload`
25202530
- `type` - a [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) of the content.\
@@ -2555,6 +2565,93 @@ For example:
25552565
},
25562566
```
25572567
2568+
<a id="option-preload-filter" name="option-preload-filter"></a>
2569+
2570+
#### `filter` option
2571+
2572+
By default, all files matching the `test` option will be preloaded.
2573+
You can use the `filter` to preload specific files individually.
2574+
2575+
The `AdvancedFilter` type provides a versatile way to define filtering logic,
2576+
from simple patterns to complex inclusion and exclusion criteria.
2577+
2578+
Here's are supported filter formats:
2579+
2580+
- `RegExp`\
2581+
The filter matches a value if it satisfies this single regular expression.\
2582+
Works the same as `{ includes: [RegExp,] }`.\
2583+
For example:
2584+
```js
2585+
filter: /vendor/, // preload files containing the `vendor` string only
2586+
```
2587+
2588+
- `Array<RegExp>`\
2589+
Matches a value if it satisfies **at least one** of the regular expressions in the array.\
2590+
Works the same as `{ includes: [RegExp1, RegExp2, ...] }`.\
2591+
For example:
2592+
```js
2593+
filter: [/vendor1/, /vendor2/,], // preload files containing the `vendor1` or `vendor2`
2594+
```
2595+
2596+
- `{ includes?: Array<RegExp>; excludes?: Array<RegExp> }`\
2597+
An object with optional `includes` and `excludes` arrays of regular expressions.\
2598+
For more advanced filtering where inclusion and exclusion criteria need to be combined:
2599+
- `includes`: Matches values that satisfy **at least one** of the regular expressions in the array.
2600+
- `excludes`: Rejects values that satisfy **at least one** of the regular expressions in the array.
2601+
- For `includes` and `excludes`, the logic is AND: a value must match the `includes` patterns and **not match** the `excludes` patterns.
2602+
2603+
For example:
2604+
```js
2605+
filter: {
2606+
includes: [/include-this/,],
2607+
excludes: [/exclude-this/,],
2608+
}
2609+
```
2610+
- `(value: string) => void | false`\
2611+
Custom logic provides maximum flexibility for filtering that cannot be expressed with regular expressions.\
2612+
A custom filter function takes an output asset file as input and decides whether it matches based on the return value:
2613+
- Returns `void` (`undefined`) or `true` if the value passes the filter.
2614+
- Returns `false` if the value fails the filter.
2615+
2616+
For example, preload all JS files except dynamically imported (async chunks).
2617+
2618+
There is the _app.js_:
2619+
2620+
```js
2621+
// specify the name of an individual chunk to exclude from preloading
2622+
import(
2623+
/* webpackChunkName: "moduleA.asyncChunk" */
2624+
'./moduleA',
2625+
);
2626+
2627+
import('./moduleB'); // other asyncChunk which should be preloaded
2628+
import './moduleC'; // normal module
2629+
```
2630+
2631+
Use the `filter` preload option to exclude files:
2632+
```js
2633+
preload: [
2634+
{
2635+
test: /\.(js|ts)$/,
2636+
filter: {
2637+
excludes: [/asyncChunk/],
2638+
},
2639+
as: 'script',
2640+
},
2641+
],
2642+
```
2643+
2644+
The same effect using the `filter` function:
2645+
```js
2646+
preload: [
2647+
{
2648+
test: /\.(js|ts)$/,
2649+
filter: (assetFile) => !/asyncChunk/.test(assetFile),
2650+
as: 'script',
2651+
},
2652+
],
2653+
```
2654+
25582655
#### Preload styles
25592656
25602657
```js
@@ -2646,6 +2743,7 @@ preload: [
26462743
>
26472744
> See [font preload](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload#what_types_of_content_can_be_preloaded).
26482745
2746+
26492747
#### Preload tags order
26502748
26512749
The generated preload tags are grouped by content type and sorted in the order of the specified `preload` options.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html-bundler-webpack-plugin",
3-
"version": "4.13.0",
3+
"version": "4.14.0",
44
"description": "Generates complete single-page or multi-page website from source assets. Build-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.",
55
"keywords": [
66
"html",

src/Common/FileUtils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,9 @@ const readDirRecursiveSync = (dir, { fs, includes = [], excludes = [] }) => {
116116
for (const file of entries) {
117117
const current = path.join(dir, file.name);
118118

119-
if (noExcludes || !excludes.find((regex) => regex.test(current))) {
119+
if (noExcludes || !excludes.some((regex) => regex.test(current))) {
120120
if (file.isDirectory()) result.push(...readDir(current));
121-
else if (noIncludes || includes.find((regex) => regex.test(current))) result.push(current);
121+
else if (noIncludes || includes.some((regex) => regex.test(current))) result.push(current);
122122
}
123123
}
124124

src/Plugin/AssetEntry.js

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -162,27 +162,8 @@ class AssetEntry {
162162
getDynamicEntry() {
163163
const { fs } = this;
164164
const dir = this.pluginOption.get().entry;
165-
const entryFilter = this.pluginOption.get().entryFilter;
166-
const isFunctionEntryFilter = typeof entryFilter === 'function';
167-
let includes = [this.pluginOption.get().test];
168-
let excludes = [];
169-
170-
if (entryFilter && !isFunctionEntryFilter) {
171-
if (entryFilter instanceof RegExp) {
172-
includes = [entryFilter];
173-
} else {
174-
if (Array.isArray(entryFilter)) {
175-
includes = entryFilter;
176-
} else {
177-
if ('includes' in entryFilter && Array.isArray(entryFilter.includes)) {
178-
includes = entryFilter.includes;
179-
}
180-
if ('excludes' in entryFilter && Array.isArray(entryFilter.excludes)) {
181-
excludes = entryFilter.excludes;
182-
}
183-
}
184-
}
185-
}
165+
const { includes: filterIncludes, excludes, fn: filterFn } = this.pluginOption.getEntryFilter();
166+
const includes = filterIncludes.length ? filterIncludes : [this.pluginOption.get().test];
186167

187168
try {
188169
if (!fs.lstatSync(dir).isDirectory()) optionEntryPathException(dir);
@@ -194,7 +175,7 @@ class AssetEntry {
194175
const entry = {};
195176

196177
files.forEach((file) => {
197-
if (isFunctionEntryFilter && entryFilter(file) === false) {
178+
if (filterFn(file) === false) {
198179
return;
199180
}
200181

src/Plugin/Option.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class Option {
6666
this.loaderPath = loaderPath;
6767
this.options = options;
6868
this.testEntry = null;
69+
this.entryFilter = options.entryFilter;
6970
this.options.css = { ...this.css, ...this.options.css };
7071
this.options.js = { ...this.js, ...this.options.js };
7172

@@ -279,6 +280,11 @@ class Option {
279280

280281
// loader tests from rules have the highest priority, over defined in plugin options
281282
this.testEntry = loaderTests.size > 0 ? [...loaderTests] : [this.options.test];
283+
this.normalizedEntryFilter = this.normalizeAdvancedFiler(this.testEntry, this.entryFilter);
284+
}
285+
286+
getEntryFilter() {
287+
return this.normalizedEntryFilter;
282288
}
283289

284290
initWatchMode() {
@@ -749,6 +755,62 @@ class Option {
749755
return renderStage;
750756
}
751757

758+
/**
759+
* Normalize the filter option defined by user and create inner structure of one.
760+
*
761+
* @param {RegExp} test
762+
* @param {AdvancedFilter} filter
763+
* @return {{includes: RegExp[], excludes: RegExp[], fn: function}}
764+
*/
765+
normalizeAdvancedFiler(test, filter) {
766+
const isFunction = typeof filter === 'function';
767+
let fn = isFunction ? filter : () => true;
768+
let includes = [];
769+
let excludes = [];
770+
771+
if (filter && !isFunction) {
772+
if (filter instanceof RegExp) {
773+
includes = [filter];
774+
} else {
775+
if (Array.isArray(filter)) {
776+
includes = filter;
777+
} else {
778+
if ('includes' in filter && Array.isArray(filter.includes)) {
779+
includes = filter.includes;
780+
}
781+
if ('excludes' in filter && Array.isArray(filter.excludes)) {
782+
excludes = filter.excludes;
783+
}
784+
}
785+
}
786+
}
787+
788+
return {
789+
includes,
790+
excludes,
791+
fn,
792+
};
793+
}
794+
795+
/**
796+
* Apply the advanced filter to a value.
797+
*
798+
* @param {string} value
799+
* @param {NormalizedAdvancedFilter} filter
800+
* @return {boolean}
801+
*/
802+
applyAdvancedFiler(value, filter) {
803+
const { includes, excludes, fn } = filter;
804+
805+
const hasIncludes = includes.length > 0;
806+
const hasExcludes = excludes.length > 0;
807+
808+
const isIncluded = !hasIncludes || includes.some((regex) => regex.test(value));
809+
const isExcluded = hasExcludes && excludes.some((regex) => regex.test(value));
810+
811+
return isIncluded && !isExcluded && fn(value) !== false;
812+
}
813+
752814
/**
753815
* Add default loader if it yet not defined.
754816
*

src/Plugin/Preload.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,11 @@ class Preload {
131131
// if the property exists and have the undefined value, exclude this attribute in generating preload tag
132132
const hasType = 'type' in conf || (conf.attributes && 'type' in conf.attributes) || optionalTypeBy.has(attrs.as);
133133

134-
// save normalized attributes
135-
conf._opts = { attrs, hasType };
134+
// filter
135+
const filter = this.pluginOption.normalizeAdvancedFiler(conf.test, conf.filter);
136+
137+
// save normalized options
138+
conf._opts = { attrs, hasType, filter };
136139

137140
groupBy[as] = [];
138141
}
@@ -148,17 +151,26 @@ class Preload {
148151
if (Array.isArray(item.chunks)) {
149152
// js
150153
for (let { chunkFile, assetFile } of item.chunks) {
151-
preloadAssets.set(assetFile, conf._opts);
154+
if (this.pluginOption.applyAdvancedFiler(assetFile, conf._opts.filter)) {
155+
preloadAssets.set(assetFile, conf._opts);
156+
}
152157
}
153158
} else {
154-
// css
155-
preloadAssets.set(item.assetFile, conf._opts);
159+
//console.log('-- assetFile: ', item);
160+
// css, images, fonts, etc
161+
if (this.pluginOption.applyAdvancedFiler(item.assetFile, conf._opts.filter)) {
162+
preloadAssets.set(item.assetFile, conf._opts);
163+
}
164+
165+
//preloadAssets.set(item.assetFile, conf._opts);
156166
}
157167

158-
// dynamic imported modules
168+
// dynamic imported modules, asyncChunks
159169
if (Array.isArray(item.children)) {
160170
for (let { chunkFile, assetFile } of item.children) {
161-
preloadAssets.set(assetFile, conf._opts);
171+
if (this.pluginOption.applyAdvancedFiler(assetFile, conf._opts.filter)) {
172+
preloadAssets.set(assetFile, conf._opts);
173+
}
162174
}
163175
}
164176
}
@@ -175,7 +187,9 @@ class Preload {
175187
? this.getPreloadFile(data.entry, assetItem.issuer, assetItem.assetFile)
176188
: assetItem.assetFile;
177189

178-
preloadAssets.set(preloadFile, conf._opts);
190+
if (this.pluginOption.applyAdvancedFiler(preloadFile, conf._opts.filter)) {
191+
preloadAssets.set(preloadFile, conf._opts);
192+
}
179193
}
180194
}
181195
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@font-face {
2+
font-family: 'OpenSans';
3+
src: url(../fonts/open-sans-regular.woff2) format('woff2');
4+
font-style: normal;
5+
}
6+
7+
h1 {
8+
color: orangered;
9+
}
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Test</title>
5+
<link rel="preload" href="../js/main.e334d98a.js" as="script">
6+
<link rel="preload" href="../js/module1.chunk.js" as="script">
7+
<link rel="preload" href="../css/style.0d25913e.css" as="style">
8+
<link rel="preload" href="../fonts/open-sans-regular.woff2" as="font" type="font/woff2" crossorigin>
9+
<link href="../css/style.0d25913e.css" rel="stylesheet">
10+
<script src="../js/main.e334d98a.js" defer="defer"></script>
11+
</head>
12+
<body>
13+
<h1>Hello World!</h1>
14+
<script src="../js/no-preload.e730d102.js" defer="defer"></script>
15+
</body>
16+
</html>

0 commit comments

Comments
 (0)