Skip to content

Commit a0c3404

Browse files
authored
Merge pull request #47 from posthtml/enhance-plugin-options
feat: allow options to customize pattern and hash length
2 parents 4543dfd + 3bdec3f commit a0c3404

4 files changed

Lines changed: 165 additions & 56 deletions

File tree

README.md

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![NPM][npm]][npm-url]
44

5-
`posthtml-hash` is a [PostHTML](https://github.com/posthtml/posthtml) plugin for hashing static assets to enable caching.
5+
`posthtml-hash` is a [PostHTML](https://github.com/posthtml/posthtml) plugin for hashing file names to enable caching.
66

77
```diff
88
<html>
@@ -29,7 +29,7 @@ npm i -D posthtml-hash
2929

3030
### Input
3131

32-
The plugin will only attempt to hash files with `[hash]` in the name.
32+
By default, the plugin will attempt to hash file names that contain `[hash]`. As a qualifier, only nodes with a `href` or `src` attribute are considered.
3333

3434
```html
3535
<html>
@@ -81,29 +81,59 @@ For convenience, you can add the post-build script to your package.json. The `po
8181
}
8282
```
8383

84+
### Custom Hash Length
85+
86+
Customize the hash length by specifying an integer after the `hash:{NUMBER}`. The default hash length is `20`.
87+
88+
**Note**: This only works for a pattern that uses square brackets and a colon separator. Use the `hashLength` option for other use cases.
89+
90+
```html
91+
<script src="src.[hash].js"></script>
92+
<!-- src.b0dcc67ffc1fd562f212.js -->
93+
94+
<script src="src.[hash:8].js"></script>
95+
<!-- src.b0dcc67f.js -->
96+
```
97+
8498
### Options
8599

86-
This plugin assumes that the file to process is in the same directory as the posthtml script. If not, specify the relative path to the html file in `options.path`:
100+
This plugin assumes that the file to process is in the same directory as the PostHTML script. If not, specify the relative path to the html file in `options.path`:
87101

88102
```js
89103
hash({
90104
/**
91105
* Relative path to processed HTML file
92106
*/
93107
path: "public", // default: ""
108+
109+
/**
110+
* File name pattern (regular expression) to match
111+
*/
112+
pattern: new RegExp(/\custom-file-pattern/), // default: new RegExp(/\[hash.*]/g)
113+
114+
/**
115+
* Custom hash length
116+
*/
117+
hashLength: 8, // default: 20
94118
});
95119
```
96120

97-
### Custom Hash Length
121+
## Recipes
98122

99-
Customize the hash length by specifying an integer after the `hash:{NUMBER}`. The default hash length is `20`.
123+
### Custom Pattern and Hash Length
100124

101-
```html
102-
<script src="src.[hash].js"></script>
103-
<!-- src.b0dcc67ffc1fd562f212.js -->
125+
```js
126+
hash({
127+
pattern: new RegExp(/\custom-file-pattern/),
128+
hashLength: 8,
129+
});
130+
```
104131

105-
<script src="src.[hash:8].js"></script>
106-
<!-- src.b0dcc67f.js -->
132+
Result:
133+
134+
```diff
135+
- <script src="script.custom-file-pattern.js"></script>
136+
+ <script src="script.b0dcc67f.js"></script>
107137
```
108138

109139
## [Examples](examples)

src/plugin.ts

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,62 @@ import { PostHTML } from "posthtml";
44
import hasha from "hasha";
55

66
const DEFAULT_HASH_LENGTH = 20;
7-
const REGEX_HASH = new RegExp(/\[hash.*]/g);
7+
const DEFAULT_PATTERN = new RegExp(/\[hash.*]/g);
88

9-
export function replaceHash(str: string, buffer: Buffer) {
10-
const match = str.match(REGEX_HASH);
9+
export function replaceHash(
10+
str: string,
11+
buffer: Buffer,
12+
exp: RegExp,
13+
hashLength: number
14+
) {
15+
const match = str.match(exp);
1116
const [_, len] = match![0].replace(/\[|]/g, "").split(":");
1217

13-
return str.replace(
14-
REGEX_HASH,
15-
hasha(buffer).slice(0, Number(len) || DEFAULT_HASH_LENGTH)
16-
);
18+
return str.replace(exp, hasha(buffer).slice(0, Number(len) || hashLength));
1719
}
1820

1921
type NodeWithHashRegex = { attrs: { href?: string; src?: string } };
2022

21-
function plugin(options?: { path?: string }) {
23+
function plugin(options?: {
24+
path?: string;
25+
hashLength?: number;
26+
pattern?: RegExp;
27+
}) {
2228
return function posthtmlHash(tree: PostHTML.Node) {
23-
tree.match(
24-
[{ attrs: { href: REGEX_HASH } }, { attrs: { src: REGEX_HASH } }],
25-
(node) => {
26-
const _node = (node as unknown) as NodeWithHashRegex;
27-
const { href, src } = _node.attrs;
29+
const exp = options?.pattern || DEFAULT_PATTERN;
30+
const hashLength = options?.hashLength || DEFAULT_HASH_LENGTH;
2831

29-
let fileName = "";
32+
tree.match([{ attrs: { href: exp } }, { attrs: { src: exp } }], (node) => {
33+
const _node = (node as unknown) as NodeWithHashRegex;
34+
const { href, src } = _node.attrs;
3035

31-
if (href) {
32-
fileName = href;
33-
} else if (src) {
34-
fileName = src;
35-
}
36+
let fileName = "";
3637

37-
const pathToFile = options?.path || "";
38-
const file = path.join(process.cwd(), pathToFile, fileName);
38+
if (href) {
39+
fileName = href;
40+
} else if (src) {
41+
fileName = src;
42+
}
3943

40-
if (fs.existsSync(file)) {
41-
const buffer = fs.readFileSync(file);
42-
const hashedFileName = replaceHash(fileName, buffer);
43-
const hashedFile = path.join(
44-
process.cwd(),
45-
pathToFile,
46-
hashedFileName
47-
);
44+
const pathToFile = options?.path || "";
45+
const file = path.join(process.cwd(), pathToFile, fileName);
4846

49-
fs.renameSync(file, hashedFile);
47+
if (fs.existsSync(file)) {
48+
const buffer = fs.readFileSync(file);
49+
const hashedFileName = replaceHash(fileName, buffer, exp, hashLength);
50+
const hashedFile = path.join(process.cwd(), pathToFile, hashedFileName);
5051

51-
if (href) {
52-
_node.attrs.href = hashedFileName;
53-
} else if (src) {
54-
_node.attrs.src = hashedFileName;
55-
}
56-
}
52+
fs.renameSync(file, hashedFile);
5753

58-
return node;
54+
if (href) {
55+
_node.attrs.href = hashedFileName;
56+
} else if (src) {
57+
_node.attrs.src = hashedFileName;
58+
}
5959
}
60-
);
60+
61+
return node;
62+
});
6163
};
6264
}
6365

src/tests/__fixtures__/original/script.oh-my-hash.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tests/plugin.test.ts

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,77 @@ import fs from "fs";
44
import path from "path";
55
import posthtml from "posthtml";
66

7+
if (!fs.existsSync("src/tests/__fixtures__/processed")) {
8+
fs.mkdirSync("src/tests/__fixtures__/processed");
9+
}
10+
711
const buffer = fs.readFileSync(
812
path.resolve(__dirname, "__fixtures__/original/bundle.min.[hash].js")
913
);
1014

11-
test.equal(replaceHash("[hash].js", buffer), "b0dcc67ffc1fd562f212.js");
15+
const DEFAULT_HASH_LENGTH = 20;
16+
const DEFAULT_PATTERN = new RegExp(/\[hash.*]/g);
17+
18+
test.equal(
19+
replaceHash("[hash].js", buffer, DEFAULT_PATTERN, DEFAULT_HASH_LENGTH),
20+
"b0dcc67ffc1fd562f212.js"
21+
);
1222
test.equal(
13-
replaceHash("script.[hash].js", buffer),
23+
replaceHash("script.[hash].js", buffer, DEFAULT_PATTERN, DEFAULT_HASH_LENGTH),
1424
"script.b0dcc67ffc1fd562f212.js"
1525
);
1626
test.equal(
17-
replaceHash("script.[hash:20].js", buffer),
27+
replaceHash(
28+
"script.[hash:20].js",
29+
buffer,
30+
DEFAULT_PATTERN,
31+
DEFAULT_HASH_LENGTH
32+
),
1833
"script.b0dcc67ffc1fd562f212.js"
1934
);
20-
test.equal(replaceHash("script.[hash:8].js", buffer), "script.b0dcc67f.js");
21-
test.throws(() => replaceHash("script.js", buffer));
22-
test.throws(() => replaceHash("script[].js", buffer));
23-
test.throws(() => replaceHash("script.[has:8].js", buffer));
24-
test.throws(() => replaceHash("script.js", buffer));
35+
test.equal(
36+
replaceHash(
37+
"script.[hash:8].js",
38+
buffer,
39+
DEFAULT_PATTERN,
40+
DEFAULT_HASH_LENGTH
41+
),
42+
"script.b0dcc67f.js"
43+
);
44+
test.throws(() =>
45+
replaceHash("script.js", buffer, DEFAULT_PATTERN, DEFAULT_HASH_LENGTH)
46+
);
47+
test.throws(() =>
48+
replaceHash("script[].js", buffer, DEFAULT_PATTERN, DEFAULT_HASH_LENGTH)
49+
);
50+
test.throws(() =>
51+
replaceHash("script.[has:8].js", buffer, DEFAULT_PATTERN, DEFAULT_HASH_LENGTH)
52+
);
53+
test.throws(() =>
54+
replaceHash("script.js", buffer, DEFAULT_PATTERN, DEFAULT_HASH_LENGTH)
55+
);
56+
57+
const CUSTOM_EXP = new RegExp(/\[oh-my-hash.*]/g);
58+
59+
test.equal(
60+
replaceHash("[oh-my-hash].js", buffer, CUSTOM_EXP, DEFAULT_HASH_LENGTH),
61+
"b0dcc67ffc1fd562f212.js"
62+
);
63+
test.equal(
64+
replaceHash("script.[oh-my-hash].js", buffer, CUSTOM_EXP, 8),
65+
"script.b0dcc67f.js"
66+
);
67+
68+
const CUSTOM_EXP_2 = new RegExp(/\oh-my-hash/);
69+
70+
test.equal(
71+
replaceHash("oh-my-hash.js", buffer, CUSTOM_EXP_2, DEFAULT_HASH_LENGTH),
72+
"b0dcc67ffc1fd562f212.js"
73+
);
74+
test.equal(
75+
replaceHash("script.oh-my-hash.js", buffer, CUSTOM_EXP_2, 8),
76+
"script.b0dcc67f.js"
77+
);
2578

2679
function copyFixture(fileName: string) {
2780
const file = path.join(__dirname, "__fixtures__/original", fileName);
@@ -53,6 +106,29 @@ async function fixture() {
53106
result.html.replace(/\n|\s+/g, ""),
54107
'<html><head><linkrel="stylesheet"href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,600i,700,700i"><linkrel="stylesheet"href="bundle.min.9a6cf95c41e87b9dc102.css"></head><body><scriptsrc="bundle.min.b0dcc67ffc1fd562f212.js"></script></body></html>'
55108
);
109+
110+
copyFixture("script.oh-my-hash.js");
111+
112+
const result2 = await posthtml()
113+
.use(
114+
hash({
115+
path: "src/tests/__fixtures__/processed",
116+
pattern: new RegExp(/\oh-my-hash/),
117+
hashLength: 8,
118+
})
119+
)
120+
.process(
121+
`<html>
122+
<body>
123+
<script src="script.oh-my-hash.js"></script>
124+
</body>
125+
</html>`
126+
);
127+
128+
test.equal(
129+
result2.html.replace(/\n|\s+/g, ""),
130+
'<html><body><scriptsrc="script.b0dcc67f.js"></script></body></html>'
131+
);
56132
}
57133

58134
fixture();

0 commit comments

Comments
 (0)